microsoft-authentication-library-for-js: Cannot update a component (`MsalProvider`)
Core Library
MSAL.js v2 (@azure/msal-browser)
Core Library Version
2.34.0
Wrapper Library
MSAL React (@azure/msal-react)
Wrapper Library Version
1.5.4
Public or Confidential Client?
Public
Description
We use a GraphQL API to provide a React SPA with access to various data across our systems. The SPA GraphQL client we’re using is Formidable’s urql - https://formidable.com/open-source/urql/
We’re trying to introduce msal
as a valid authentication method and we can successfully acquire tokens silently and use those for access to the backend. The urql graphQL operation handling can be extended by providing callback functions for specific purposes. We’re providing a callback that requests the token and then adds it to the urql operation.
However using the urql useQuery
hook in the same pattern we have used elsewhere, we get a Cannot update a component (
MsalProvider) while rendering a different component error
- this occurs regardless of whether the token is existing in cache or newly acquired.
I’ve provided a stripped down index.tsx
to demonstrate the issue.
Error Message
Msal Logs
MSAL Configuration
export const msalConfig: Configuration = {
auth: {
clientId: `${process.env.REACT_APP_WEB_CLIENT_ID}`,
authority: `https://login.microsoftonline.com/${
process.env.REACT_APP_TENANT_ID
}`,
redirectUri: `${process.env.REACT_APP_REDIRECT_URI ?? "http://localhost:3000"}`,
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
}
Relevant Code Snippets
This is a single index.tsx incorporating the MSAL components used, the urql Provider and a simple query:
import { useEffect } from "react"
import ReactDOM from "react-dom/client"
import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"
import { InteractionType, PublicClientApplication } from "@azure/msal-browser"
import { useQuery, Operation, makeOperation } from "urql"
// self-host fonts
import "@fontsource/inter"
import "./index.css"
import { GRAPHQL_URL, APP_MODE } from "./conf/config"
import { msalConfig } from "./conf/ssoConfig"
import { createClient, Provider, fetchExchange } from "urql"
import { contextExchange } from "./exchanges/contextExchange"
import { FetchingLabel } from "./common/FetchingLabel"
import { UhOh } from "./common/UhOh"
console.log(`App mode is ${APP_MODE}`)
console.log(`Using graphql at ${GRAPHQL_URL}`)
const SITES = `
query utags {
sites {
id
name
description
}
}
`
const msalInstance = new PublicClientApplication(msalConfig)
// get the msal access token from cache or acquire a new one
const acquireAccessToken = async (): Promise<string> => {
const activeAccount = msalInstance.getActiveAccount() // This will only return a non-null value if you have logic somewhere else that calls the setActiveAccount API
const accounts = msalInstance.getAllAccounts()
if (!activeAccount && accounts.length === 0) {
console.error("No accounts found. Please login.")
/*
* User is not signed in. Throw error or wait for user to login.
* Do not attempt to log a user in outside of the context of MsalProvider
*/
}
const request = {
scopes: ["User.Read"],
account: activeAccount || accounts[0],
}
const authResult = await msalInstance.acquireTokenSilent(request)
return authResult.accessToken
}
// add the access token to the exchange operation context
const addTokenToContext = async (operation: Operation) => {
// if operation is not a query or mutation, return the operation
if (operation.kind !== "query" && operation.kind !== "mutation") {
return operation.context
}
const token = await acquireAccessToken()
const fetchOptions =
typeof operation.context.fetchOptions === "function"
? operation.context.fetchOptions()
: operation.context.fetchOptions || {}
return makeOperation(operation.kind, operation, {
...operation.context,
fetchOptions: {
...fetchOptions,
headers: {
...fetchOptions.headers,
Authorization: `Bearer ${token}`,
},
},
}).context
}
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
const exchanges = [
contextExchange({
getContext: addTokenToContext,
}),
fetchExchange,
]
// setup urql client for use with msal
const client = createClient({
url: GRAPHQL_URL,
exchanges,
})
root.render(
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect}>
<Provider value={client}>
<SimpleQuery />
</Provider>
</MsalAuthenticationTemplate>
</MsalProvider>,
)
export function SimpleQuery() {
const [result] = useQuery({
query: SITES,
})
const { data, fetching, error } = result
useEffect(() => {
if (data?.sites) {
console.log(data.sites)
}
}, [data])
if (fetching) {
return <FetchingLabel>Fetching universal tags</FetchingLabel>
}
if (error) {
return <UhOh error={error}>We were unable to find any Sites</UhOh>
}
return <div>Simple Query</div>
}
### Reproduction Steps
1. `npm start` with a React CRA setup
### Expected Behavior
We would expect the MSALProvider not to generate a React error on silent token request
### Identity Provider
Azure AD / MSA
### Browsers Affected (Select all that apply)
Chrome
### Regression
_No response_
### Source
External (Customer)
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 1
- Comments: 15 (3 by maintainers)
After some investigations, I have found a better workaround for relay applications like mine and @eberridge.
The fix is the ensure that the call to acquireTokenSilent never happen during the react render phase. Here’s a version of the Relay project setup sample modified with the fix.
Closing as it’s not clear this is an issue with MSAL. If anyone disagrees please feel free to open a new issue thanks!
We faced the same problem when using the method useSuspenseQuery from the library (Tanstack Query). @kawazoe fix works well.
Oh wow @kawazoe – I can’t tell you how many hours I’ve spent fighting this exact same issue on different projects where I was using msal-react! Thank you!! Adding
await Promise.resolve();
is exactly what’s needed. I’m guessing the underlying issue is thatacquireTokenSilent()
completes synchronously for the typical case, at which point the MSAL instance (which we’re of course referencing in the component tree via<MsalProvider />
) is being updated while still in the same MicroTask as a React rendering step.In my case I’m using Jotai and trying to initialize an atom asynchronously on first use with an API call (that requires getting a token from MSAL), but I’ve also seen this issue pop up with Recoil.js. No amount of
useEffect(...)
-foo was ever really helpful, and in one case I ended up tearing out msal-react altogether in frustration and managing authentication entirely outside of the React tree.@tnorling Could you add some guidance for this type of scenario in the msal-react tutorial step/documentation for getting an access token?
Interesting. FWIW we migrated away from
urql
to the Apollo client and all the MsalProvider issues went away.