auth0/auth0-react

`useAuth0` reloads state after every token fetch

githorse opened this issue · 6 comments

Checklist

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