在React上下文中由于在useEffect内部进行的setState而导致的无限循环

huangapple go评论57阅读模式
英文:

Infinite loop in react context caused by a setState inside a useEffect

问题

我在启用了被注释掉的代码行后,从这个上下文组件中得到了无限循环。我已经完全隔离了这个组件,但问题仍未解决。

只有在行被折扣并且浏览器的 cookies 中有令牌时,才会发生这种行为。

useEffect 中的代码并不会无限重复,只有顶层的代码会。

Auth

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>
    )
}
英文:

I'm getting infinite looping from this context component as soon as I enable the commented out line. I already completely isolated the component and still the problem is not solved.

This behavior only occurs when the row is discounted and the browser has the token in its cookies.

The code inside useEffect doesn't repeat infinitely, just the code at the top level.

Auth

&#39;use client&#39;
import { AxiosError } from &#39;axios&#39;
import { useRouter } from &#39;next/router&#39;
import { destroyCookie, parseCookies, setCookie } from &#39;nookies&#39;
import {
ReactNode,
createContext,
useCallback,
useEffect,
useMemo,
useState
} from &#39;react&#39;
import { Either, left, right } from &#39;@core/logic/Either&#39;
import { accessLevel, controllers, endpoints } from &#39;@routes/backend&#39;
import { Api } from &#39;@services/api/Axios&#39;
import { fetchLogin } from &#39;@services/api/FetchLogIn&#39;
export type TSystemUser = {
name: string
role: string
} | null
export type TLoginParams = {
phoneNumber: string
password: string
}
export type TLoginResponse = Either&lt;unknown, unknown&gt;
type TAuthContext = {
systemUser: TSystemUser
login: (data: TLoginParams) =&gt; Promise&lt;Either&lt;unknown, unknown&gt;&gt;
logout: () =&gt; void
}
export const AuthContext = createContext({} as TAuthContext)
export async function AuthProvider({ children }: { children: ReactNode }) {
const [systemUser, setSystemUser] = useState&lt;TSystemUser&gt;(null)
const [accessToken, setAccessToken] = useState&lt;string | null&gt;(null)
const { &#39;nextauth-token&#39;: token } = parseCookies()
if ((!accessToken &amp;&amp; token) || accessToken !== token) {
setAccessToken(token)
Api.defaults.headers.authorization = token
}
if (accessToken &amp;&amp; !token) {
destroyCookie(undefined, &#39;nextauth-token&#39;)
Api.defaults.headers.authorization = null
}
console.log(&#39;a&#39;) // &#39;a&#39; &#39;a&#39; &#39;a&#39; &#39;a&#39; ...
const fecthSystemUserInfo = useCallback(async (): Promise&lt;
Either&lt;AxiosError&lt;TSystemUser&gt;, TSystemUser&gt;
&gt; =&gt; {
try {
const response = await Api.get&lt;TSystemUser&gt;(
accessLevel.session +
controllers.withSession +
endpoints.RetrieveUserInformation
)
return right(response.data)
} catch (err) {
const error = err as AxiosError&lt;TSystemUser&gt;
switch (error.status) {
default:
return left(error)
}
}
}, [accessToken])
useEffect(() =&gt; {
async function retrieveUserInformation(): Promise&lt;void&gt; {
const response = await fecthSystemUserInfo()
if (response.isRight()) {
const user = response.value
console.log(user) // { name: &#39;User&#39;, role: &#39;Admin&#39; }
// await setSystemUser(user) // commented line
console.log(systemUser) // null
}
console.log(response.value)
}
if (!systemUser &amp;&amp; accessToken) {
retrieveUserInformation()
}
if (systemUser &amp;&amp; !accessToken) {
setSystemUser(null)
}
console.log(systemUser)
}, [accessToken])
const login = useCallback(
async ({
phoneNumber,
password
}: TLoginParams): Promise&lt;TLoginResponse&gt; =&gt; {
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, &#39;nextauth-token&#39;, 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 =&gt; {
const router = useRouter()
destroyCookie(null, &#39;nextauth-token&#39;)
Api.defaults.headers.authorization = null
setSystemUser(null)
router.push(&#39;/&#39;)
}, [accessToken])
const contextValue = useMemo(
() =&gt; ({ systemUser, login, logout }),
[accessToken]
)
return (
&lt;AuthContext.Provider value={contextValue}&gt;
{children}
&lt;/AuthContext.Provider&gt;
)
}

RootLayout

import &#39;@styles/Globals/Globals.css&#39;
import { ReactNode } from &#39;react&#39;
import Footer from &#39;@components/Footer/Footer&#39;
import Header from &#39;@components/Header/Header&#39;
import { AuthProvider } from &#39;@contexts/Auth/Auth&#39;
import styles from &#39;@styles/Components/MainLayout.module.css&#39;
export default function RootLayout({ children }: { children: ReactNode }) {
return (
&lt;AuthProvider&gt;
&lt;html lang=&quot;pt-br&quot;&gt;
&lt;body&gt;
&lt;div className={styles.app}&gt;
&lt;Header /&gt;
{children}
&lt;Footer /&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;/AuthProvider&gt;
)
}

I expected the function inside useEffect to update the user (and it does), however this causes an infinite loop. Another behavior I would expect is that components using this context would be updated, and this is not the case.

答案1

得分: 1

if ((!accessToken && token) || accessToken !== token) {
   setAccessToken(token)
   Api.defaults.headers.authorization = token
}

这段代码将在每次组件渲染时运行,它会更新访问令牌的状态(setAccessToken(token)),因此当组件重新渲染时,useEffect 将会运行,因为 accessToken 包含在其依赖数组中,然后 useEffect 更新了 systemUsersetSystemUser(user)),因此这将再次重新渲染组件,由于systemUser的新值与旧值不同,您可能认为它们是相同的,但两个对象始终不相等,当组件重新渲染时,再次发生相同的情况。

当您注释掉 setSystemUser(user) 时,当 useEffect 运行时,它将不会触发重新渲染,因为它不会更新任何状态,因此您不会陷入无限重新渲染的问题。


为了解决这个问题,您不应该在组件的主体内设置访问令牌,而是在组件首次挂载时使用 useEffect 内部设置它:

useEffect(() => {
  if (token) {
    setAccessToken(token);
    Api.defaults.headers.authorization = token;
  }
}, []);

现在,您可以取消注释该行而不会导致无限重新渲染。

英文:
if ((!accessToken &amp;&amp; token) || accessToken !== token) {
   setAccessToken(token)
   Api.defaults.headers.authorization = token
}

this code will run at every component render and it is updating the access token state (setAccessToken(token)) so when the component rerenders useEffect will run since accessToken is included in its dependency array then useEffect is updating systemUser (setSystemUser(user)) so this will rerender the component again, since the new value of systemUser is different of the old one, you may think it is the same but two objects are always not equal, and when the component rerenders, again, the same will happen.<br>

when you comment setSystemUser(user) then when useEffect runs it will not trigger a rerender because it does not update any state so you don't run into an infinite rerender


to handle this you don't want to set the access token in the body of your component but inside a useEffect that runs when the component first mounts:

useEffect(() =&gt; {
  if (token) {
    setAccessToken(token);
    Api.defaults.headers.authorization = token;
  }
}, []);

now you can uncomment the line without getting an infinite render.

huangapple
  • 本文由 发表于 2023年6月22日 05:40:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76527335.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定