skip to Main Content

I load my settings from the server:

import { Context, createContext, useEffect, useState } from "react";

export class AppSettings {
    buildName: string = '';
    authority: string = '';
    truthPortalApi: string = '';
    environment: string = '';
}

export const AppSettingsContext: Context<AppSettings> = createContext<AppSettings>(null!)

export const AppSettingsProvider: React.FC<{ children: JSX.Element }> = ({ children }) => {
    const [settings, setSettings] = useState<AppSettings>(new AppSettings())

    useEffect(() => {
        fetch(`${window.location.origin}/settings`)
            .then(response => response.json())
            .then(data => setSettings(data))
    }, []);

    return (
        <AppSettingsContext.Provider value={settings}>
            {children}
        </AppSettingsContext.Provider>
    )
}

and then I use them:

...
export const AuthContext: Context<AuthService> = createContext<AuthService>(null!);

export const AuthProvider: React.FC<{ children: JSX.Element[] }> = ({ children }) => {
    let appSettings: AppSettings = useContext<AppSettings>(AppSettingsContext)!;
    const [authService, setAuthService] = useState<AuthService>(new AuthService(appSettings));

    return (
        <AuthContext.Provider value={authService}>
        { children }
        </AuthContext.Provider>
    );
}

export function useAuth(): AuthService {
    return useContext(AuthContext);
}

However, something that uses auth via let auth: AuthService = useAuth(); only gets the inital emty value. It doesn’t get the real value after it’s loaded.
Can this work in React?

2

Answers


  1. Chosen as BEST ANSWER

    The package oidc-client-ts seems to be the least broken, but you have to wrap it like this in order to pass in the settings.

    import { Fragment, useEffect, useState } from "react";
    
    import { AuthContextProps, AuthProvider, hasAuthParams, useAuth } from 'react-oidc-context';
    import { User, } from "oidc-client-ts";
    import { AppSettings, useAppSettings } from "./appSettings";
    import Loading from "./components/Loading/Loading";
    
    import { NavigateFunction, useNavigate } from "react-router";
    
    let url: string = `${window.location.origin}/`;
    
    export const LocalAuthProvider: React.FC<{ children: JSX.Element | JSX.Element[] }> = ({ children }) => {
        let appSettings: null | AppSettings = useAppSettings();
    
        let [loading, setLoading] = useState<boolean>(true);
    
        useEffect(() => {
            setLoading((appSettings?.authority ?? '') === '');
    
        }, [appSettings]);
    
        // Copied from https://github.com/authts/react-oidc-context/blob/f175dcba6ab09871b027d6a2f2224a17712b67c5/src/AuthProvider.tsx#L20-L30
        let signinCallback = (_user: User | void): void => {
            window.history.replaceState(
                {},
                document.title,
                window.location.pathname
            )
        };
    
        return (
            <Loading loading={loading || (appSettings?.authority ?? '') === ''}>
                {loading || (appSettings?.authority ?? '') === '' ? (null) : (
                    <AuthProvider //userManager={userManager} doesn't work, all other settings must be passed
                        authority={appSettings!.authority}
                        client_id={'truth_portal'}
                        redirect_uri={url}
                        silent_redirect_uri={`${url}silent-callback.html`}
                        post_logout_redirect_uri={url}
                        response_type={'code'}
                        scope={'openid profile offline_access truth_portal'}
                        loadUserInfo={true}
                        onSigninCallback={signinCallback}
                    >
                        {children}
                    </AuthProvider>
                )}
            </Loading>
        );
    }
    

  2. You are not updating the authService when appSettings change. This line only sets the initial value once (although it recreates a new AuthService on each render).

    const [authService, setAuthService] = useState<AuthService>(new AuthService(appSettings));
    

    You need a useEffect to create a new AuthService when appSettings change:

    useEffect(() => {
      setAuthService(new AuthService(appSettings));
    }, [appSettings]);
    

    In addition, you should avoid creating redundant AuthService on each render by using lazy initial values function, which would only create the initial AuthService on mount:

    const [authService, setAuthService] = useState<AuthService>(
      () => new AuthService(appSettings) // lazy initial value
    );
    

    Example:

    export const AuthProvider: React.FC<{ children: JSX.Element[] }> = ({ children }) => {
        let appSettings: AppSettings = useContext<AppSettings>(AppSettingsContext)!;
        const [authService, setAuthService] = useState<AuthService>(
          () => new AuthService(appSettings) // lazy initial value
        );
        
        // update on mount and when appSettings change
        useEffect(() => {
          setAuthService(new AuthService(appSettings));
        }, [appSettings]);
    
        return (
            <AuthContext.Provider value={authService}>
            { children }
            </AuthContext.Provider>
        );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search