Home > Software engineering >  React context not updating from after promise completed
React context not updating from after promise completed

Time:09-11

I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.

Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.

As a bit of additional context to this, I am using turborepo and next.js.

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;

Theoretically, once the authentication is finished I would expect the props.children to display.

CodePudding user response:

I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.

Therefore the context is stuck on the static default values.

Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.

There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.

  • Related