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

Next.js+Turborepo中React Context Promise完成后未更新认证状态

问题:isAuthenticating状态无法更新,认证完成后未显示目标内容

使用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

  1. 重构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;
  1. 在页面或根组件中按正确层级嵌套:
// 页面或根组件示例
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_

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 15:40:22