`useAuth0` reloads state after every token fetch
githorse opened this issue · 6 comments
Checklist
- The issue can be reproduced in the auth0-react sample app (or N/A).
- I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
- I have looked into the API documentation and have not found a suitable solution or answer.
- I have searched the issues and have not found a suitable solution or answer.
- I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- I agree to the terms within the Auth0 Code of Conduct.
Description
I recently migrated from @auth0/auth0-react
1.2.0
to 2.2.0
and noticed a major difference in the behavior of the useAuth0
hook and/or Auth0Provider
. Before the upgrade, the hook / context re-renders 2-3 times on initial startup of my app -- i.e., when it is logging in. After the upgrade, it re-renders after every access token fetch.
Was this change between 1.2.0
and 2.2.0
by design? (I can't offhand see any compelling reason why it needs to do this, since getAccessTokeSilently()
returns a promise in any case.) How can I load access tokens into the Auth0Provider
without causing it to re-render?
Reproduction
I can't really demonstrate this without hitting a real auth0 account somewhere. But the basic idea is this:
function Root() {
return (
<Auth0Provider
domain='my-domain'
clientId='my-client-id'
authorizationParams={myAuthorizationParams}
>
<MyApp />
</Auth0Provider>
)
}
const TokensIWillNeed = [
{scope: 'scope1', audience: 'audience1'},
{scope: 'scope2', audience: 'audience2'},
{scope: 'scope3', audience: 'audience3'}
]
function MyApp() {
const auth = useAuth0()
useEffect(
() => TokensIWillNeed.map(authorizationParams => auth.fetchAccessTokenSilently({authorizationParams}),
[]
)
console.log(`rendering App`, auth) // renders 4+ times
return (
<div>app goes here</div>
)
}
Additional context
Since fetching an access token can take 2-3 seconds or more, it adds a significant delay to any API call I need to make. I avoid that by pre-fetching in parallel all the tokens I might need anywhere in the app at initial startup, in a top-level component. This used to work fine, but after the upgrade, it now causes excessive re-renders on at app startup -- basically, it makes the page flicker a bunch of times as tokens start arriving.
auth0-react version
2.2.0
React version
18.2.0
Which browsers have you tested in?
Chrome
Hi @githorse - thank for raising this
getAccessTokeSilently
with a new audience is effectively logging the user in again, and so new user means new react context, which results in the renders. In v1 we ignored the new user unless their "updated_at" prop changed, but this lead to issues like #347. So the behaviour you're seeing in v2 is expected.
You can work around this using one of the solutions discussed in facebook/react#14110 (comment). Using https://github.com/dai-shi/use-context-selector for example, you could do something like useContextSelector(Auth0Context, v => v.user?.updated_at)
(instead of useAuth0
) to get the v1 behaviour back.
Thanks @adamjmcgrath. Sounds like there were good reasons behind that decision.
I'm working through your suggestion now. I'm not quite sure how it will play out because I actually do need the auth
object at this same level (to call and fetch my app's settings from an API before rendering anything). So I'll have to think about how to re-architect this. (Maybe I can use useContextSelector
to pluck just the fetchAccessTokenSilently
method, not sure.)
However, a more immediate problem is that I can't get useContextSelector
to work. If I try something like this:
import {Auth0Provider, Auth0Context} from '@auth0/auth0-react'
import {useContextSelector} from 'use-context-selector'
function Root() {
return (
<Auth0Provider
domain='my-domain'
clientId='my-client-id'
authorizationParams={myAuthorizationParams}
>
<MyApp />
</Auth0Provider>
)
}
function MyApp() {
const isAuthenticated = useContextSelector(Auth0Context, auth => auth.isAuthenticated)
return (
<div>app goes here</div>
)
}
... I get an error "useContextSelector requires special context
". Ok, I guess I need to create the context using use-context-selector
's special createContext
method:
import {initialContext, Auth0ContextInterface, Auth0Provider} from '@auth0/auth0-react'
import {createContext, useContextSelector} from 'use-context-selector'
const MyAuth0Context = createContext<Auth0ContextInterface>(initialContext)
...
<Auth0Provider
context={MyAuth0Context}
domain={MyDomain}
...
/>
... but this gives me a type error -- Auth0Provider
doesn't like that version of context.
Do you have any guidance on how this should work?
Hey @githorse
I need to create the context using use-context-selector's special createContext method:
Ah, I didn't realise this - let me test a working recommendation and get back to you.
Hi @githorse - I've put a working example here https://github.com/auth0/auth0-react/compare/custom-context
You don't need use-context-selector
, just create your own context that consumes the auth0 context and memoizes it as you like, eg
import { Auth0Provider, useAuth0, Auth0ContextInterface, initialContext } from '@auth0/auth0-react'
export const MyAuth0Context =
createContext<Auth0ContextInterface>(initialContext);
const MyAuth0Provider = ({ children }: { children?: React.ReactNode }) => {
const { user, ...rest } = useAuth0();
const contextValue = useMemo<Auth0ContextInterface<User>>(() => {
return {
user,
...rest,
};
}, [user?.updated_at, rest.isLoading, rest.isAuthenticated]);
return (
<MyAuth0Context.Provider value={contextValue}>
{children}
</MyAuth0Context.Provider>
);
};
function Root() {
return (
<Auth0Provider
domain='my-domain'
clientId='my-client-id'
authorizationParams={myAuthorizationParams}
>
<MyAuth0Provider>
<MyApp />
</MyAuth0Provider>
</Auth0Provider>
)
}
function MyApp() {
const isAuthenticated = useContext(MyAuth0Context)
return (
<div>app goes here</div>
)
}
It works! ... but apparently I don't understand React. Since you're still calling useAuth0
inside MyAuth0Provider
, which wraps MyApp
, I would have thought this would still re-render just as many times -- but it does not. Please, teach me your ways.
Am just using useMemo
to stop you from rendering child components, per https://react.dev/reference/react/useMemo#skipping-re-rendering-of-components
(but yeah, react rendering can be git of a dark art - and there's usually a bit of trial and error when I'm trying to figure these things out)
Closing as #616 (comment) should answer your question