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

2

6

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>
);

Hinrichs answered 27/9, 2022 at 18:47 Comment(9)
Sorry, it's not very apparent how the msalInstance instance and MsalProvider component relates to any of the routing code. Is there some code or functionality that was inadvertently omitted that shows any coupling? Can you clarify what you are trying to do here and what the issue is? See minimal reproducible example.Seesaw
Sure! The new feature (loader) of React Router allows you to fetch data that the route (screen or component) requires, like in my app for editing the existing user. Before this new feature, you can do it with useEffect hook, however this new set of features like error, loaders, and elements like Form for handling submission through the routes can be very helpful and lead to a cleaner solution. The problem, or my problem, is that the loader is just a plain function that does not have, or I don't know how to, access any state or value(like what I need to fetch data using MSAL) inside the loaderBlondellblondelle
reactrouter.com/en/main/route/loaderBlondellblondelle
I wasn't asking what a loader is, my question was more to do with your code, how the MSAL code related to the routing code. Are you basically asking how to access msalInstance in the loader function? It looks like you've defined some loader function already with it enclosed.Seesaw
Are you basically asking how to access msalInstance in the loader function? Thank you. Yes, that is my question. Please any advice?Blondellblondelle
Why can't you access it? Is it not in scope? Or do you need help getting in scope?Seesaw
The msl instance is defined in the index.tsx of my application and passed to the msal provider. that way I can have access to the single instance by using hooks provided by the msal library inside any component below the tree of components of the providers. that is not incorrect, right? but then, this loader function does not have access to any of that. What i'm missing here?Blondellblondelle
I think, in summary, the instance is managed within react. The loader is outside react's scope and then I don't know how to get inside the loader function the msal instance. makes any sence?Blondellblondelle
Let us continue this discussion in chat.Blondellblondelle
S
6

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

    ...
  };
Seesaw answered 27/9, 2022 at 22:6 Comment(1)
Thanks for the Answer! Also for bringing this Currying technique that I didn't understand completely but I will study it more in deep for sure.Blondellblondelle
T
1

When implementing the accepted solution, you may run into this error:

uninitialized_public_client_application: You must call and await the initialize function before attempting to call any other MSAL API. For more please visit aka.ms/msaljs/browser-errors.

MS provides a solution for this if you visit the link provided in the above error message.

So in the original example, in index.ts, you just need one additional line:

const msalInstance = new PublicClientApplication(msalConfig);

// NEW: Required before attempting any other MSAL api call:
await msalInstance.initialize();

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>
);
Tsushima answered 14/6, 2024 at 17:25 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.