You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
    )
}

问题原因与解决方案

核心问题

  1. 组件顶层直接调用状态更新:每次渲染时都会执行setAccessToken的条件判断,当Cookie存在token时,accessToken状态更新触发重渲染,重渲染又会再次执行状态更新,形成无限循环。
  2. useEffect依赖项缺失:未加入systemUser,无法响应其变化,导致逻辑执行不完整。
  3. Context值依赖错误:contextValue只依赖accessToken,systemUser变化时Context不会更新,下游组件无法同步状态。
  4. AuthProvider被定义为异步函数:React组件不能是异步函数,会导致渲染异常。

修复步骤

  1. 将token同步逻辑移到useEffect中:避免顶层渲染时直接触发状态更新。
  2. 补全useEffect依赖项:加入systemUser和fecthSystemUserInfo,确保逻辑能响应所有相关状态变化。
  3. 修正Context值依赖:加入systemUser、login、logout,保证状态更新时Context同步更新。
  4. 移除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 05:55:03