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 =>