Search code examples
auth0apollo-clientreact-context

Detecting if Apollo has finished configuration before allowing queries


I have a React SPA with following top-level structure. I'm using Auth0 for authentication and Apollo Client for queries and React Context to provide a global state.

The problem I'm having is that a query made in my global state is executing before the Apollo Client can complete configuration of its header (namely, inserting a token via Auth0).

Is there a way (perhaps using the useApolloClient hook to tell if Apollo has a token in its authLink? Or another way to achieve the objective of delaying initialization of Global State until its parent component AuthorizedApolloProvider has completed?

<BrowserRouter>
  <Auth0ProviderWithHistory>
    <AuthorizedApolloProvider>
      <GlobalState>
        <App />
      </GlobalState>
    </AuthorizedApolloProvider>
  </Auth0ProviderWithHistory>
</BrowserRouter>

Within AuthorizedApolloProvider, I am setting context for Apollo by inserting a token on all queries as follows:

const AuthorizedApolloProvider = ({children}) => {

  const { getAccessTokenSilently } = useAuth0()

  const httpLink = createHttpLink ({
    uri: uriGQL, // a config parameter
  })
  
  const authLink = setContext(async () => {
    const token = await getAccessTokenSilently()
    console.log('Got the token..... ', token.substring(0, 10))
    return {
      headers: {
        authorization: token ? token : ""
      }
    }
  })

  let links = [authLink, httpLink]

  const client = new ApolloClient({
    link: ApolloLink.from(links),
    cache: new InMemoryCache({
      addTypename: false,
    }),
  })

  console.log('Rendering ApolloProvider')

  return(
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  )
}

Next, in Global State, I am sending a GQL query to fetch information about the user who has logged in as follows:


const GlobalState = props => {

  const { user: auth0User, isLoading, isAuthenticated } = useAuth0()

  const client = useApolloClient()

  const [state, setState] = useState(initialState)

  const [ getUser, { loading, data, error }] = useLazyQuery(GET_USER)
  
  const history = useHistory()


  useEffect(async () => {

    // Has useAuth0 returned?
    if(isLoading===false) {
      console.log('isAuthenticated=', isAuthenticated)
      console.log('auth0User', auth0User)

      // Is authenticated?
      if(!isAuthenticated) {
        // If not authenticated...
        console.log('Not authenticated')
        localStorage.removeItem('user')
        setState({ ...state, loaded: true, user: null })
      }
      else {
        // If authenticated...
        // Check to see if user object is in local storage
        let user = await localStorage.getItem('user')
        if(user) {
          console.log('Authenticated, found user in local storage')
          // User found in local storage
          user = JSON.parse(user)
          setState({ ...state, loaded: true, user: user })
        }
        else {
          // User not found in local storage. Must fetch from server
          console.log('Authenticated, getting user from server')

          try {
            console.log('Calling getUser...')
            const result = await getUser()
          }
          catch (error) {
            console.log(error)
          }
        }
      }
    }

  }, [isLoading])

...

The problem I'm having is that the getUser query is being called before the Apollo client can finish configuring.

When I run the code, I see that the below fragment...

try {
   console.log('Calling getUser...')
   const result = await getUser()
}

...is being executed before the following...

console.log('Got the token..... ', token.substring(0, 10))
    return {
      headers: {
        authorization: token ? token : ""
      }
    }

Solution

  • You need to await getAccessTokenSilently completion in a blocking manner. You can achieve this by using state.

    const [token, setToken] = useState();
    
    useEffect(()=>{
     try {
      const token = await getAccessTokenSilently(); // you need this to finish before moving on
      setToken(token);
     } catch (e){
      //catch errors
     }
    }, []);
    
    if(!token) return '...loading'; //render loading component
    
    const authLink = setContext(() => {
     //token comes from state now & don't need await
    })
    
    const links = [authLink, httpLink];
    
    const client = new ApolloClient({
     link: ApolloLink.from(links),
     cache: new InMemoryCache({ addTypename: false })
    })
    
    return(
     <ApolloProvider client={client}>
      {children}
     </ApolloProvider>
    )
    

    I personally put my ApollClient, links, and other related ApolloConfig in a different file and pass it a token to clean this up a bit.