Search code examples
reactjstypescriptreact-routerazure-ad-msal

React + MSAL + React Router 6.4.1 (latest version) loader feature question


Dear fellow React developers.

Can someone explain or show what should be the best approach whilst using MSAL in conjunction with React Router in its current latest version 6.4.1 when you want to use the new loader feature?

Based on the new loader feature that you can read about here:

"Each route can define a "loader" function to provide data to the route element before it renders."

https://reactrouter.com/en/main/route/loader

The question arises in me because I'm lacking in imagination here... the only way I can recall to have access to the msal instance is by using the hooks the MSAL library provides when on a component inside the MsalProvider. However, the new loader feature relies on a plain function where I can't use hooks or access any application state.

Any help will be very welcome. Thanks in advance.

To give a little context below I share some of my application code.

in my UserEditScreen.tsx:


// UserEditScreen component definition

....

// loader definition outside UserEditScreen component

export const loader = ({ request, params }: LoaderFunctionArgs) => {
...
  // some more code to clarify my need on the msal instance inside the loader
  const request = {
      account: msalInstance.getActiveAccount(),
      scopes: scopes
    }
    const accessToken = await msalInstance.acquireTokenSilent(request)
      .then(response => response.accessToken)
      .catch(error => {        
        console.error(error);
      })
// then with the accessToken I can just use made 
// a javascript fetch request to my secured endpoint in azure

 ...
}

in my app.tsx

type AppProps = {
  msalInstance: PublicClientApplication;
}

const router = createBrowserRouter(createRoutesFromElements(
  <Route path="/" errorElement={<GlobalError />}>
    ...
    <Route path="/administration" element={
      <AuthenticatedScreen>
        <AdministrationScreen />
      </AuthenticatedScreen>
    }>
      <Route path='users' element={<UsersScreen />} />
      <Route path='users/new' element={<UserNewScreen />} />
      <Route path='users/:id/edit'
        element={<UserEditScreen />}
        loader={userLoader}
        errorElement={<UserEditError />}
      />
    </Route>
    ...
  </Route>

));

function App({ msalInstance }: AppProps) {

  return (
    <MsalProvider instance={msalInstance}>
      <RouterProvider router={router} />
    </MsalProvider>
  );
}

export default App;

in my index.tsx

const msalInstance = new PublicClientApplication(msalConfig);
const accounts = msalInstance.getAllAccounts();

if (accounts.length > 0) {
  msalInstance.setActiveAccount(accounts[0]);
}

msalInstance.addEventCallback((event: EventMessage) => {
  if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
    const payload = event.payload as AuthenticationResult;
    const account = payload.account;
    msalInstance.setActiveAccount(account);
  }
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <App msalInstance={msalInstance} />
  </React.StrictMode>
);


Solution

  • Instead of creating the router component you could convert it to a function that takes the msalInstance as an argument to close it over in scope and returns the configured router.

    Example:

    App

    type AppProps = {
      msalInstance: PublicClientApplication;
    }
    
    interface CreateRouter {
      msalInstance: PublicClientApplication;
    }
    
    const createRouter = ({ msalInstance }: CreateRouter) => createBrowserRouter(
      createRoutesFromElements(
        <Route path="/" errorElement={<GlobalError />}>
          ...
          <Route path="/administration" element={
            <AuthenticatedScreen>
              <AdministrationScreen />
            </AuthenticatedScreen>
          }>
            <Route path='users' element={<UsersScreen />} />
            <Route path='users/new' element={<UserNewScreen />} />
            <Route path='users/:id/edit'
              element={<UserEditScreen />}
              loader={userLoader(msalInstance)}
              errorElement={<UserEditError />}
            />
          </Route>
          ...
        </Route>
      )
    );
    
    function App({ msalInstance }: AppProps) {
      const router = React.useMemo(
        () => createRouter({ msalInstance }),
        [msalInstance]
      );
    
      return (
        <MsalProvider instance={msalInstance}>
          <RouterProvider router={router} />
        </MsalProvider>
      );
    }
    
    export default App;
    

    UserEditScreen

    Similarly the loader function can be updated to a curried function that takes the msalInstance object as an argument and returns the loader function with the msalInstance object closed over in scope.

    // loader definition outside UserEditScreen component
    export const loader = (msalInstance: PublicClientApplication) => 
      ({ request, params }: LoaderFunctionArgs) => {
        ...
        // some more code to clarify my need on the msal instance inside the loader
        const request = {
          account: msalInstance.getActiveAccount(),
          scopes: scopes
        }
        const accessToken = await msalInstance.acquireTokenSilent(request)
          .then(response => response.accessToken)
          .catch(error => {        
            console.error(error);
          })
        // then with the accessToken I can just use made 
        // a javascript fetch request to my secured endpoint in azure
    
        ...
      };