This page looks best with JavaScript enabled

GraphQL made simple with React Suspense and Hooks

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

react suspense graphql


Although still considered experimental, React Suspense is certainly worth trying, even if for smaller personal projects.

Suspense helps control component life cycles declaratively and prevent race conditions, among other things.

Using Suspense and hooks, we can write clean and functional code to manage state and handle errors while fetching GraphQL data.

Overview of React Suspense

One of the primary use cases of Suspense is managing the application state during async operations. With Suspense, you avoid writing annoying imperative code, like conditional rendering, and try/catch statements inside of your components.

Another well-known use case is code-splitting , but we won’t cover it in this post.

In a typical modern UI, there are tons of things to take care of when fetching data. You have to toggle loaders, prevent race conditions, and maintain the correct order of events. Suspense makes all of it infinitely simpler. Here’s how you would typically use React Suspense:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const initialResource = fetchProfileData();


function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails resource={initialResource} />
    </Suspense>
  );
}

function ProfileDetails({ resource }) {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

One interesting thing you might’ve noticed is that we create an initialResource object right when the app loads. So what is this resource anyway? Let’s briefly cover resources, they are pretty important.

Resources are sources of async data for Suspense. Under the hood, a resource is simply an object that has a read method. React Suspense will call that method as needed for its inner workings.

So what exactly does Suspense do for us?

Notice how we call resource.user.read() without any safe checking. Getting user info is an async process, yet we’re using the resource synchronously because we know that Suspense will take care of fetching and checking the data. All we need to do is provide Suspense with a fallback component.

This approach makes our application cleaner and more robust by eliminating the need for safe checking and conditional rendering and preventing race conditions.

Suspense with GraphQL

We’ll build a simple app that displays SpaceX rocket launches using their public GraphQL endpoint .

react suspense app tutorial
Displaying SpaceX rocket launches

We’ll be using Apollo client , just because it’s much easier to work with in my experience. But you could do the same thing using any other GraphQL client.

Adding apollo client

First, let’s set up our Apollo client that we will use to send queries.

1
2
3
4
5
6
import { ApolloClient, InMemoryCache } from "@apollo/client";

export const client = new ApolloClient({
  uri: "https://api.spacex.land/graphql",
  cache: new InMemoryCache()
});

Adding graphql query

Now we will add the query for fetching launches. To follow good practices, we’ll store it in a separate file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { gql } from "@apollo/client";

export const GET_LAUNCHES = gql`
  query GetLaunches {
    launchesPast(limit: 10) {
      mission_name
      launch_date_local
      launch_site {
        site_name_long
      }
      rocket {
        rocket_name
      }
    }
  }
`;

Factory function

This is where Suspense comes into play. Let’s make a factory function that accepts a GraphQL query and returns a resource:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export const wrapGraphQL = (query) => {
  let status = "pending";
  let result = null;
  const suspender = client
    .query({
      query,
      fetchPolicy: "network-only"
    })
    .then((response) => {
      status = "success";
      result = response.data;
    })
    .catch((err) => {
      status = "error";
      result = err;
    });

  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else {
        return result;
      }
    }
  };
};

Our factory function tracks the status and the server response of the async request. We also have a suspender that stores the GraphQL query promise. When we hear back from the server, we update the status and result variables.

So the factory function returns a resource, and, as I mentioned, a resource is simply an object with the read method. The read method does the following:

  • If the status is pending, read throws the suspender promise, which Suspense will catch and display the fallback component.
  • If the status is completed, read returns the result.
  • If the status is error, ‘readthrows theresult`, which, in that case, is an error instance.
    Believe it or not, that was the most complex piece.

useAPI hook

Strictly speaking, we could start using wrapGraphql directly without wrapping it into a hook. But a custom hook is a nice abstraction and is handy if we decide to add things later, like checking session status or permissions before sending a request.

1
2
3
4
5
6
7
export const useAPI = (query) => {
  const data = useMemo(() => {
    return wrapGraphQL(query);
  }, [query]);

  return { data };
};

The code for useAPI is straightforward. The hook accepts a query and uses the wrapGraphql factory function to return a new resource.

Finally, here’s how we would use it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Suspense } from "react";
import { GET_LAUNCHES } from "./queries";
import { useAPI } from "./useAPI";

const Launches = ({ launches }) => {
  return (
    <div>
      {launches.read().launchesPast.map((item) => (
        <div>
          <h1>{item.mission_name}</h1>
          <p>{item.rocket.rocket_name}</p>
        </div>
      ))}
    </div>
  );
};

export default function App() {
  const { data } = useAPI(GET_LAUNCHES);

  return (
    <div className="App">
      <Suspense fallback={<div>Loading...</div>}>
        <Launches launches={data} />
      </Suspense>
    </div>
  );
}

Conclusion

As a side note: we used Apollo for the GraphQL client, however, another alternative to consider is Relay . Relay is closely integrated with React Suspense and could be a better choice for you. With that said, it’s also much more opinionated, and I find Apollo easier to use.

That’s it for this post. We used React Suspense with GraphQL, but you can certainly adapt it to work with any other async source. Suspense also has more applications than just handling async operations. The best place to begin your learning is the official Suspense overview doc.

Here’s a link to the codesandbox for this tutorial.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.