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 : ""
}
}
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.