React Context中useEffect内setState引发无限循环问题求助
问题描述
注释掉AuthContext组件useEffect中的setSystemUser(user)后出现无限循环,已隔离组件仍未解决。仅当浏览器Cookie存在token时触发,且useEffect内部代码并未重复执行,只有组件顶层代码持续运行。
原本预期:
- useEffect中的函数能更新用户状态(实际已更新)且不引发无限循环
- 使用该Context的组件能同步更新状态
这两点均未实现。
Auth组件代码
'use client' import { AxiosError } from 'axios' import { useRouter } from 'next/router' import { destroyCookie, parseCookies, setCookie } from 'nookies' import { ReactNode, createContext, useCallback, useEffect, useMemo, useState } from 'react' import { Either, left, right } from '@core/logic/Either' import { accessLevel, controllers, endpoints } from '@routes/backend' import { Api } from '@services/api/Axios' import { fetchLogin } from '@services/api/FetchLogIn' export type TSystemUser = { name: string role: string } | null export type TLoginParams = { phoneNumber: string password: string } export type TLoginResponse = Either<unknown, unknown> type TAuthContext = { systemUser: TSystemUser login: (data: TLoginParams) => Promise<Either<unknown, unknown>> logout: () => void } export const AuthContext = createContext({} as TAuthContext) export async function AuthProvider({ children }: { children: ReactNode }) { const [systemUser, setSystemUser] = useState<TSystemUser>(null) const [accessToken, setAccessToken] = useState<string | null>(null) const { 'nextauth-token': token } = parseCookies() if ((!accessToken && token) || accessToken !== token) { setAccessToken(token) Api.defaults.headers.authorization = token } if (accessToken && !token) { destroyCookie(undefined, 'nextauth-token') Api.defaults.headers.authorization = null } console.log('a') // 'a' 'a' 'a' 'a' ... const fecthSystemUserInfo = useCallback(async (): Promise< Either<AxiosError<TSystemUser>, TSystemUser> > => { try { const response = await Api.get<TSystemUser>( accessLevel.session + controllers.withSession + endpoints.RetrieveUserInformation ) return right(response.data) } catch (err) { const error = err as AxiosError<TSystemUser> switch (error.status) { default: return left(error) } } }, [accessToken]) useEffect(() => { async function retrieveUserInformation(): Promise<void> { const response = await fecthSystemUserInfo() if (response.isRight()) { const user = response.value console.log(user) // { name: 'User', role: 'Admin' } // await setSystemUser(user) // commented line console.log(systemUser) // null } console.log(response.value) } if (!systemUser && accessToken) { retrieveUserInformation() } if (systemUser && !accessToken) { setSystemUser(null) } console.log(systemUser) }, [accessToken]) const login = useCallback( async ({ phoneNumber, password }: TLoginParams): Promise<TLoginResponse> => { try { if (systemUser) { return right(null) } const response = await fetchLogin({ phoneNumber, password }) if (response.isLeft()) { return left(response.value) } const { accessToken, user } = response.value setSystemUser(user) setCookie(undefined, 'nextauth-token', accessToken.token, { expires: new Date(accessToken.expiresIn) }) return right(null) } catch (error) { console.log(error) return left(JSON.stringify(error, null, 2)) } }, [accessToken] ) const logout = useCallback((): void => { const router = useRouter() destroyCookie(null, 'nextauth-token') Api.defaults.headers.authorization = null setSystemUser(null) router.push('/') }, [accessToken]) const contextValue = useMemo( () => ({ systemUser, login, logout }), [accessToken] ) return ( <AuthContext.Provider value={contextValue}> {children} </AuthContext.Provider> ) }
RootLayout组件代码
import '@styles/Globals/Globals.css' import { ReactNode } from 'react' import Footer from '@components/Footer/Footer' import Header from '@components/Header/Header' import { AuthProvider } from '@contexts/Auth/Auth' import styles from '@styles/Components/MainLayout.module.css' export default function RootLayout({ children }: { children: ReactNode }) { return ( <AuthProvider> <html lang="pt-br"> <body> <div className={styles.app}> <Header /> {children} <Footer /> </div> </body> </html> </AuthProvider> ) }
问题原因与解决方案
核心问题
- 组件顶层直接调用状态更新:每次渲染时都会执行
setAccessToken的条件判断,当Cookie存在token时,accessToken状态更新触发重渲染,重渲染又会再次执行状态更新,形成无限循环。 - useEffect依赖项缺失:未加入
systemUser,无法响应其变化,导致逻辑执行不完整。 - Context值依赖错误:
contextValue只依赖accessToken,systemUser变化时Context不会更新,下游组件无法同步状态。 - AuthProvider被定义为异步函数:React组件不能是异步函数,会导致渲染异常。
修复步骤
- 将token同步逻辑移到useEffect中:避免顶层渲染时直接触发状态更新。
- 补全useEffect依赖项:加入
systemUser和fecthSystemUserInfo,确保逻辑能响应所有相关状态变化。 - 修正Context值依赖:加入
systemUser、login、logout,保证状态更新时Context同步更新。 - 移除AuthProvider的async关键字:React组件必须是同步函数,异步逻辑放在useEffect中处理。
修复后的Auth组件代码
'use client' import { AxiosError } from 'axios' import { useRouter } from 'next/router' import { destroyCookie, parseCookies, setCookie } from 'nookies' import { ReactNode, createContext, useCallback, useEffect, useMemo, useState } from 'react' import { Either, left, right } from '@core/logic/Either' import { accessLevel, controllers, endpoints } from '@routes/backend' import { Api } from '@services/api/Axios' import { fetchLogin } from '@services/api/FetchLogIn' export type TSystemUser = { name: string role: string } | null export type TLoginParams = { phoneNumber: string password: string } export type TLoginResponse = Either<unknown, unknown> type TAuthContext = { systemUser: TSystemUser login: (data: TLoginParams) => Promise<Either<unknown, unknown>> logout: () => void } export const AuthContext = createContext({} as TAuthContext) // 移除async关键字 export function AuthProvider({ children }: { children: ReactNode }) { const [systemUser, setSystemUser] = useState<TSystemUser>(null) const [accessToken, setAccessToken] = useState<string | null>(null) const fecthSystemUserInfo = useCallback(async (): Promise< Either<AxiosError<TSystemUser>, TSystemUser> > => { try { const response = await Api.get<TSystemUser>( accessLevel.session + controllers.withSession + endpoints.RetrieveUserInformation ) return right(response.data) } catch (err) { const error = err as AxiosError<TSystemUser> return left(error) } }, [accessToken]) // 处理token与accessToken的同步逻辑 useEffect(() => { const { 'nextauth-token': token } = parseCookies() if ((!accessToken && token) || accessToken !== token) { setAccessToken(token) Api.defaults.headers.authorization = token } if (accessToken && !token) { destroyCookie(undefined, 'nextauth-token') Api.defaults.headers.authorization = null setAccessToken(null) } }, [accessToken]) // 处理用户信息获取与状态同步 useEffect(() => { async function retrieveUserInformation(): Promise<void> { const response = await fecthSystemUserInfo() if (response.isRight()) { const user = response.value setSystemUser(user) } } if (!systemUser && accessToken) { retrieveUserInformation() } if (systemUser && !accessToken) { setSystemUser(null) } }, [accessToken, systemUser, fecthSystemUserInfo]) const login = useCallback( async ({ phoneNumber, password }: TLoginParams): Promise<TLoginResponse> => { try { if (systemUser) { return right(null) } const response = await fetchLogin({ phoneNumber, password }) if (response.isLeft()) { return left(response.value) } const { accessToken: newAccessToken, user } = response.value setSystemUser(user) setAccessToken(newAccessToken.token) Api.defaults.headers.authorization = newAccessToken.token setCookie(undefined, 'nextauth-token', newAccessToken.token, { expires: new Date(newAccessToken.expiresIn) }) return right(null) } catch (error) { console.log(error) return left(JSON.stringify(error, null, 2)) } }, [systemUser] ) const logout = useCallback((): void => { const router = useRouter() destroyCookie(null, 'nextauth-token') Api.defaults.headers.authorization = null setSystemUser(null) setAccessToken(null) router.push('/') }, []) const contextValue = useMemo( () => ({ systemUser, login, logout }), [systemUser, login, logout] ) return ( <AuthContext.Provider value={contextValue}> {children} </AuthContext.Provider> ) }
内容的提问来源于stack exchange,提问作者Prinsqty
相关产品推荐
相关产品推荐

