NextJS中useLayoutEffect不等待异步函数及状态更新疑问
页面刷新时的登录校验问题及状态更新疑问
场景代码
_app.tsx 中的登录校验逻辑
const { authenticated, setAuthenticated } = useContext(AuthContext) const fetchRefresh = async () => { const status = await AuthStore.checkAuth() if (status == 200) { setAuthenticated && setAuthenticated(true) } else { setAuthenticated && setAuthenticated(false) } return status } useLayoutEffect(() => { if (localStorage.getItem("access_token")) { fetchRefresh() } }, [])
Layout 组件中的状态校验与重定向
export const withLayout = <T extends Record<string, unknown> & IAuthContext>(Component: FunctionComponent<T>) => { return function withLayoutComponent(props: T): JSX.Element { const { authenticated } = useContext(AuthContext) const router = useRouter() useEffect(() => { if (!authenticated) router.push("/auth/login") }, [authenticated]) return ( <Layout> <Component {...props} /> </Layout> ); }; };
登录页也存在对应的useEffect逻辑:若用户已登录则重定向至"/"页面。
问题1:页面刷新时的闪烁与多次重定向
控制台执行顺序如下:
useLayoutEffect(_app.tsx)use effect (index.tsx)complete the fetchRefresh function
问题根源在于登录校验接口是异步的:页面刷新时初始authenticated为false,Layout会先触发重定向到登录页;等接口返回确认用户已登录后,登录页的useEffect又会重定向回首页,最终导致页面闪烁、多次跳转。
解决方案
1. 在AuthContext中新增加载状态
给上下文添加isLoading状态,标记登录校验请求的完成状态:
// AuthContext 定义示例 const AuthContext = createContext<{ authenticated: boolean; setAuthenticated: (val: boolean) => void; isLoading: boolean; setIsLoading: (val: boolean) => void; }>({ authenticated: false, setAuthenticated: () => {}, isLoading: true, setIsLoading: () => {}, });
2. 修改_app.tsx的校验逻辑
在请求开始和结束时更新isLoading:
const { authenticated, setAuthenticated, isLoading, setIsLoading } = useContext(AuthContext) const fetchRefresh = async () => { setIsLoading(true) try { const status = await AuthStore.checkAuth() setAuthenticated(status === 200) } finally { setIsLoading(false) } } useLayoutEffect(() => { if (localStorage.getItem("access_token")) { fetchRefresh() } else { setIsLoading(false) } }, [])
3. 调整Layout的重定向逻辑
仅当加载完成且用户未登录时,才执行重定向;加载过程中显示占位内容避免闪烁:
export const withLayout = <T extends Record<string, unknown> & IAuthContext>(Component: FunctionComponent<T>) => { return function withLayoutComponent(props: T): JSX.Element { const { authenticated, isLoading } = useContext(AuthContext) const router = useRouter() useEffect(() => { if (!isLoading && !authenticated) { router.push("/auth/login") } }, [authenticated, isLoading]) if (isLoading) { return <div>加载中...</div> } return ( <Layout> <Component {...props} /> </Layout> ); }; };
4. 同步修改登录页逻辑
同样判断isLoading,避免提前触发重定向:
// 登录页示例 const { authenticated, isLoading } = useContext(AuthContext) const router = useRouter() useEffect(() => { if (!isLoading && authenticated) { router.push("/") } }, [authenticated, isLoading]) if (isLoading) { return <div>加载中...</div> } // 登录页渲染代码
问题2:为何setAuthenticated(true)后打印authenticated仍为false?
React的状态更新是异步批量更新机制:调用setAuthenticated后,当前作用域中的authenticated变量是组件上一次渲染的快照,不会立即更新,只有等组件重新渲染后,才能拿到最新的状态值。
如果需要在状态更新后执行逻辑,可通过两种方式实现:
- 使用
useEffect监听authenticated变化:
useEffect(() => { console.log(authenticated) // 状态更新后会触发此处 }, [authenticated])
- 在异步函数中直接使用接口返回的结果:
const fetchRefresh = async () => { const status = await AuthStore.checkAuth() const isAuth = status === 200 setAuthenticated(isAuth) console.log(isAuth) // 直接使用该变量而非authenticated return status }
内容的提问来源于stack exchange,提问作者Михаил Васильев
相关产品推荐
相关产品推荐

