Home > Software design >  Gatsby making component await for nested async in useEffect hook
Gatsby making component await for nested async in useEffect hook

Time:05-26

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 form other examples that:

  1. Signs-in and gets a JWT from Auth0 and saves it in local storage; and then
  2. 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:

  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.

  1. Component mount (at this point token is still empty)
  2. When token value gets changed

CodePudding user response:

You can execute a query when a pre-condition has been met using Pausing useQuery

Add a Loading state to AuthApolloProvider and change the state in useEffect when the client is being loaded.

AuthApolloProvider.js

....
const [client, setClient] = useState(null);
const [loading, setLoading] = useState(true);
....

useEffect(() => {
  if (client) setLoading(false);
}, [client])

OrganizationList.js pass loading state from "AuthApolloProvider" to "useOrganizations"

const { organizations, loading } = useOrganizations(loading);

useOrganization.js

 const [{ data, fetching }] = useQuery({ query: GET_ORGANIZATIONS,  pause: loading })
  • Related