From 96dad278ea6d5ad79eb30006de4be1bdb9f4fadc Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 4 Mar 2026 19:05:34 -0800 Subject: [PATCH] =?UTF-8?q?fix(admin-web):=20=E4=BF=AE=E5=A4=8D=20React=20?= =?UTF-8?q?#310=20=E2=80=94=20useState=20=E5=BF=85=E9=A1=BB=E5=9C=A8?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=20return=20=E5=89=8D=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:expandedKeys 的 useState 调用位于 "if (!isAuthenticated) return null" 之后,违反 Rules of Hooks。认证状态变化时 hooks 调用数量不一致, React 报 #310 "Cannot update a component while rendering a different component"。 修复:将 activeKey 计算和 expandedKeys useState 全部移至条件 return 之前, 确保每次渲染 hooks 调用顺序完全一致。 Co-Authored-By: Claude Sonnet 4.6 --- .../admin-web/src/layouts/AdminLayout.tsx | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/frontend/admin-web/src/layouts/AdminLayout.tsx b/frontend/admin-web/src/layouts/AdminLayout.tsx index 181ce98..bd7148e 100644 --- a/frontend/admin-web/src/layouts/AdminLayout.tsx +++ b/frontend/admin-web/src/layouts/AdminLayout.tsx @@ -100,28 +100,14 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children const { isAuthenticated, isLoading, user, logout } = useAuth(); const [collapsed, setCollapsed] = useState(false); - // 未登录 → 跳转 /login - // - // ⚠️ 必须在 useEffect 中执行,不能直接在 render 函数体内调用 router.replace()。 - // 原因:render 期间调用 router 会触发父组件的状态更新(React #310 错误): - // "Cannot update a component while rendering a different component" - // - // useEffect 在 commit 阶段(DOM 更新后)执行,此时可以安全地触发导航。 - // 依赖数组包含 router 是为了满足 exhaustive-deps lint 规则,router 引用稳定不会重复触发。 - useEffect(() => { - if (!isLoading && !isAuthenticated) { - router.replace('/login'); - } - }, [isLoading, isAuthenticated, router]); - - // 加载中或未登录时显示空白 - if (isLoading || !isAuthenticated) return null; - // Derive activeKey from current pathname + // ⚠️ 必须在所有 useState/useEffect 之前计算,因为 expandedKeys 的初始值依赖它 const activeKey = pathname.replace(/^\//, '') || 'dashboard'; - // Auto-expand parent nav items based on current path - const getInitialExpanded = () => { + // ⚠️ 所有 useState / useEffect 必须在任何条件 return 之前调用(Rules of Hooks) + // 如果放在 "if (!isAuthenticated) return null" 之后,每次认证状态变化时 + // hooks 调用数量不一致,React 会抛出 #310 错误。 + const [expandedKeys, setExpandedKeys] = useState(() => { const expanded: string[] = []; navItems.forEach(item => { if (item.children && item.children.some(c => activeKey.startsWith(c.key) || activeKey === item.key)) { @@ -129,8 +115,17 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children } }); return expanded; - }; - const [expandedKeys, setExpandedKeys] = useState(getInitialExpanded); + }); + + // 未登录 → 跳转 /login(useEffect 在 commit 阶段执行,避免 render 期间触发状态更新) + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.replace('/login'); + } + }, [isLoading, isAuthenticated, router]); + + // 加载中或未登录时显示空白(条件 return 必须在所有 hooks 之后) + if (isLoading || !isAuthenticated) return null; const toggleExpand = (key: string) => { setExpandedKeys(prev =>