Next.js+Turborepo中React Context Promise完成后未更新认证状态
使用React Context管理认证逻辑,基于Next.js + Turborepo搭建。当前遇到isAuthenticating属性值无法更新的问题:认证流程完成后,isAuthenticating始终保持true,导致无法通过isAuthenticating === false && authenticatedUser !== undefined判断用户已认证,最终无法显示预期内容。但相同逻辑在CodeSandbox中可正常运行。
相关代码
AuthenticationContext
import { SilentRequest } from '@azure/msal-browser'; import { useMsal } from '@azure/msal-react'; import { User } from 'models'; import { createContext, useCallback, useContext, useEffect, useState } from "react"; import { msal, sendRequest } from 'utils'; interface AuthenticationContextType { authenticatedUser?: User; isAuthenticating: boolean; } const AuthenticationContext = createContext<AuthenticationContextType>({ authenticatedUser: undefined, isAuthenticating: true }); export const AuthenticationProvider = (props: { children: React.ReactNode }) => { const { accounts, instance } = useMsal(); const [user, setUser] = useState<User>(); const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true); const [currentAccessToken, setCurrentAccessToken] = useState<string>(); const getUserFromToken = useCallback(async () => { if (user) { setIsAuthenticating(false); return; } const userRequest = await sendRequest('me'); if (! userRequest.error && userRequest.data) { setUser(userRequest.data as User); } }, [user]); const getAccessToken = useCallback(async () => { if (! currentAccessToken) { const request: SilentRequest = { ...msal.getRedirectRequest(), account: accounts[0] } const response = await instance.acquireTokenSilent(request); setCurrentAccessToken(response.accessToken); } return getUserFromToken(); }, [accounts, currentAccessToken, getUserFromToken, instance]); useEffect(() => { async function initialiseAuthentication() { await getAccessToken(); setIsAuthenticating(false); } initialiseAuthentication(); }, [getAccessToken]); return ( <AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}> { props.children } </AuthenticationContext.Provider> ) } export const useAuth = () => { const context = useContext(AuthenticationContext); if (context === undefined) { throw new Error("useAuth was used outside of it's provider.") } return context; }
AuthenticationLayout
import { useEffect, useState } from 'react'; import { AuthenticationProvider, useAuth } from '../hooks/authentication'; import MsalLayout from './msal-layout'; const AuthenticationLayout = (props: { children: React.ReactNode }) => { const { isAuthenticating, authenticatedUser } = useAuth(); const wasAuthenticationSuccessful = () => { return ! isAuthenticating && authenticatedUser !== undefined; } const renderContent = () => { if (! wasAuthenticationSuccessful()) { return ( <p>You are not authorized to view this application.</p> ) } return props.children; } if (isAuthenticating) { return ( <p>Authenticating...</p> ) } return ( <MsalLayout> { renderContent() } </MsalLayout> ) } export default AuthenticationLayout;
MsalLayout
import { InteractionType } from '@azure/msal-browser'; import { AuthenticatedTemplate, MsalAuthenticationTemplate, MsalProvider, } from "@azure/msal-react"; import { msalInstance, msal } from 'utils'; import { AuthenticationProvider } from '../hooks/authentication'; msal.initialize(); const MsalLayout = (props: { children: React.ReactNode }) => { return ( <MsalProvider instance={msalInstance}> <MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}> <AuthenticatedTemplate> <AuthenticationProvider> {props.children} </AuthenticationProvider> </AuthenticatedTemplate> </MsalAuthenticationTemplate> </MsalProvider> ) } export default MsalLayout;
问题分析与修复方案
核心问题1:组件层级错误,useAuth无法获取Provider的状态
当前AuthenticationLayout在AuthenticationProvider的上层(AuthenticationProvider嵌套在MsalLayout内部),导致AuthenticationLayout中调用useAuth时,只能拿到Context的默认值(isAuthenticating: true),永远无法获取到Provider更新后的状态。
修复方法:调整组件嵌套结构
将AuthenticationProvider移到AuthenticationLayout的外层,确保useAuth所在组件处于Provider的覆盖范围内。修改后的结构应该是:MsalProvider -> MsalAuthenticationTemplate -> AuthenticatedTemplate -> AuthenticationProvider -> AuthenticationLayout
- 重构
AuthenticationLayout,移除内部的MsalLayout引用:
// 修改后的AuthenticationLayout import { useAuth } from '../hooks/authentication'; const AuthenticationLayout = (props: { children: React.ReactNode }) => { const { isAuthenticating, authenticatedUser } = useAuth(); const wasAuthenticationSuccessful = () => { return !isAuthenticating && authenticatedUser !== undefined; }; if (isAuthenticating) { return <p>Authenticating...</p>; } if (!wasAuthenticationSuccessful()) { return <p>You are not authorized to view this application.</p>; } return props.children; }; export default AuthenticationLayout;
- 在页面或根组件中按正确层级嵌套:
// 页面或根组件示例 import AuthenticationLayout from './AuthenticationLayout'; import MsalLayout from './MsalLayout'; const App = () => { return ( <MsalLayout> <AuthenticationLayout> {/* 你的应用内容 */} </AuthenticationLayout> </MsalLayout> ); }; export default App;
核心问题2:isAuthenticating状态更新逻辑不完整
在AuthenticationProvider的getUserFromToken函数中,只有当user已存在时才设置isAuthenticating: false;但当通过请求获取到新用户数据并调用setUser后,没有同步更新isAuthenticating,且initialiseAuthentication中的setIsAuthenticating(false)会在user状态更新前执行,导致出现isAuthenticating: false但authenticatedUser: undefined的情况。
修复方法:完善状态更新逻辑
修改getUserFromToken,在成功获取用户信息或请求失败时,都结束认证加载状态:
const getUserFromToken = useCallback(async () => { if (user) { setIsAuthenticating(false); return; } try { const userRequest = await sendRequest('me'); if (!userRequest.error && userRequest.data) { setUser(userRequest.data as User); } } catch (err) { console.error('获取用户信息失败:', err); } finally { // 无论成功失败,都结束认证加载状态 setIsAuthenticating(false); } }, [user]);
同时移除initialiseAuthentication中的setIsAuthenticating(false),避免重复设置:
useEffect(() => { async function initialiseAuthentication() { await getAccessToken(); // 移除这里的setIsAuthenticating(false) } initialiseAuthentication(); }, [getAccessToken]);
额外优化:避免getAccessToken重复触发
getAccessToken的依赖项包含currentAccessToken,当currentAccessToken更新时,getAccessToken会重新创建,导致useEffect重复执行。可以调整逻辑,移除currentAccessToken的依赖:
const getAccessToken = useCallback(async () => { // 直接用instance获取token,避免依赖currentAccessToken导致重复触发 const request: SilentRequest = { ...msal.getRedirectRequest(), account: accounts[0] }; const response = await instance.acquireTokenSilent(request); setCurrentAccessToken(response.accessToken); return getUserFromToken(); }, [accounts, getUserFromToken, instance]);
内容的提问来源于stack exchange,提问作者leb_

