Updated
As @shahroon-farooqi demonstrated below this pattern is well documented by urql here including rationale for the wonka library dependency.
Updated working demo on GitHub.
I'm new to React and still having trouble working out how the async awaits fit together with respect to hooks.
I have a minimal Gatsby/React starter project (my full code on GitHub here) that I have pieced together from other examples that:
- Signs-in and gets a JWT from Auth0 and saves it in local storage; and then
- Includes that JWT in a GraphQL request to fetch and display a list of Organizations.
When this is processed in the browser as 2 separate page loads (ie I first click sign-in and I'm then redirected to the view the list of Organizations page) it works as expected. But once I'm already signed-in and I click the browser refresh button on the list of Organizations page, I can see that GraphQL fetch fails because it is called before the client has had a chance to load the JWT into the header. The JWT is successfully loaded a split second later but I need this to happen before the GraphQL fetch is attempted.
The auth and client is passed as a wrapper:
// gatsby-browser.js
export const wrapRootElement = ({ element }) => {
return (
<Auth0Provider
domain={process.env.GATSBY_AUTH0_DOMAIN}
redirectUri={process.env.GATSBY_AUTH0_REDIRECT_URI}
...
>
<AuthApolloProvider>{element}</AuthApolloProvider>
</Auth0Provider>
);
}
And the client is set up with the JWT like this:
// src/api/AuthApolloProvider.tsx
const setupClient = (_token) => {
return createClient({
url: process.env.GATSBY_HASURA_GRAPHQL_URL,
fetchOptions: () => {
return {
headers: {
authorization: _token ? `Bearer ${_token}` : "",
},
};
},
});
};
const AuthApolloProvider = ({ children }) => {
const { getAccessTokenSilently, isAuthenticated, getIdTokenClaims } =
useAuth0();
const [token, setToken] = useState("");
const [client, setClient] = useState(setupClient(token));
useEffect(() => {
(async () => {
if (isAuthenticated) {
const tokenClaims = await getIdTokenClaims();
setToken(tokenClaims.__raw);
}
})();
}, [isAuthenticated, getAccessTokenSilently]);
useEffect(() => {
setClient(setupClient(token));
}, [token]);
return <Provider value={client}>{children}</Provider>;
};
I then have this contoller to get the list of Organizations:
// src/controllers/Organizations.ts
import { useQuery } from "urql"
const GET_ORGANIZATIONS = `
query get_organizations {
organizations {
name
label
}
}
`
export const useOrganizations = () => {
const [{ data, fetching }] = useQuery({ query: GET_ORGANIZATIONS })
return {
organizations: data?.organizations,
loading: fetching,
}
}
And finally my list of Organizations component:
// src/components/Organizations/OrganizationList.tsx
import { useOrganizations } from "../../controllers/Organizations";
const OrganizationList = () => {
const { organizations, loading } = useOrganizations();
return (
<>
{loading ? (
<p>Loading...</p>
) : (
organizations.map((organization: OrganizationItemType) => (
<OrganizationItem
organization={organization}
key={organization.name}
/>
))
)}
</>
);
};
So as I understand it, I don't want to make the useOrganizations()
call in the component until the async method inside AuthApolloProvider
has completed and successfully loaded the JWT into the client.
Because I'm new to React and I've pieced this together from other examples I'm not sure how to approach this - any help would be great.
CodePudding user response:
I have modified your ApolloProvider. You will need to add Wonka, as urql utilises the Wonka library.
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { pipe, map, mergeMap, fromPromise, fromValue } from "wonka";
import {
createClient,
Provider,
dedupExchange,
cacheExchange,
fetchExchange,
Exchange,
Operation,
} from "urql";
interface AuthApolloProviderProps {
children: React.ReactChildren;
}
const fetchOptionsExchange =
(fn: any): Exchange =>
({ forward }) =>
(ops$) => {
return pipe(
ops$,
mergeMap((operation: Operation) => {
const result = fn(operation.context.fetchOptions);
return pipe(
(typeof result.then === "function"
? fromPromise(result)
: fromValue(result)) as any,
map((fetchOptions: RequestInit | (() => RequestInit)) => ({
...operation,
context: { ...operation.context, fetchOptions },
}))
);
}),
forward
);
};
const AuthApolloProvider = ({ children }: AuthApolloProviderProps) => {
const { getAccessTokenSilently, getIdTokenClaims } = useAuth0();
const url = process.env.GATSBY_HASURA_GRAPHQL_URL;
let client = null;
if (url) {
client = createClient({
url: url,
exchanges: [
dedupExchange,
cacheExchange,
fetchOptionsExchange(async (fetchOptions: any) => {
await getAccessTokenSilently({
audience: process.env.GATSBY_AUTH0_AUDIENCE,
scope: "openid profile email offline_access",
ignoreCache: true,
});
const tokenClaims = await getIdTokenClaims();
const token = tokenClaims?.__raw;
return Promise.resolve({
...fetchOptions,
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
});
}),
fetchExchange,
],
});
} else {
throw new Error("url not define");
}
return <Provider value={client}>{children}</Provider>;
};
export default AuthApolloProvider;
CodePudding user response:
useEffect(() => {
if (token.length === 0) return
setClient(setupClient(token))
}, [token])
You probably don't want to setClient/setupClient when token is empty? Code inside useEffect gets executed at least 2 time.
- Component mount (at this point token is still empty)
- When token value gets changed