React Suspense is relatively new feature that allows developers to create logic to show substitute components when data loads into the main component. But does it really make a difference for React devs and simplify code structure?

What is the purpose of React Suspense?

From my understanding, the main goal of this component is to reduce code boilerplate to manage loading states when an application fetches data from asynchronous sources such as network requests.

Examples provided by React and an explanation of this component may be found in the official documentation: https://react.dev/reference/react/Suspense.

The goal of this article is to share my thoughts about using this feature in production.

Replace loading state with Suspense

Its first obvious application displays a replacement component when data is loading from the network. All the examples below will use the Appollo GraphQL server for network requests.

I implemented a similar logic in both components, one with the useQuery hook and another with useSuspenseQuery.

The useQuery approach:

function ClassicLoad({ skip = 0, take = 5 }: ClassicLoadProps) {
  const { loading, error, data } = useQuery(GET_ITEMS, {
    variables: {
      skip,
      take,
    },
    returnPartialData: true,
  })

  if (loading) return <Loading />
  if (error) return <p>Error : {error.message}</p>

  return (
    <ul className={styles.container}>
      {data!.items.map((item) => (
        <li className={styles.listItem} key={item.id}>
          <span className={`${styles.itemField} ${styles.idField}`}>
            {item.id}
          </span>
          <span className={styles.itemField}>{item.title}</span>
        </li>
      ))}
    </ul>
  )
}

Here, we pass to the component pagination options and invoke request to the GraphQL server. While request is processing (I implemented a delay on the server side, to see the loading state on the local machine), we show the loading component. In this piece of code we rely on the loading state from the useQuery hook and conditional rendering to show the loading component.

The useSuspenseQuery approach:

function SuspenseLoad({ skip = 0, take = 5 }: SuspendLoadProps) {
  const { data, error } = useSuspenseQuery(GET_ITEMS, {
    variables: {
      skip,
      take,
    },
  })

  if (error) return <p>Error : {error.message}</p>

  return (
    <ul className={styles.container}>
      {data?.items?.map((item) => (
        <li className={styles.listItem} key={item?.id}>
          <span className={`${styles.itemField} ${styles.idField}`}>
            {item?.id}
          </span>
          <span className={styles.itemField}>{item?.title}</span>
        </li>
      ))}
    </ul>
  )
}

In this function we use the same query, but we don’t use loading state. Instead, we use the Suspense component to show the loading state.

We can see use case of both components in the following code snippet:

  <div className={styles.loadContainer}>
    <ClassicLoad />
    <Suspense fallback={<Loading />}>
      <SuspenseLoad />
    </Suspense>
  </div>

With a closer look at both components, they look mostly the same. However, in the SuspenseLoad component, we have one line less - we don’t use loading variable and pass spinner handling to the parent component.

How the UI looks like with both components:

Two components with a single request

For me, it seems like an improvement. We focus more on the component logic and spinner handling we pass on to the component with composition code.

Stacking components

Now, let’s consider a stack of components with request logic. In this case, I will consume the same endpoint with the items list but display two lists with different items.

<div className={styles.loadContainer}>
  <div className={styles.loadStack}>
    <ClassicLoad />
    <ClassicLoad skip={5} take={5} />
  </div>
  <Suspense fallback={<Loading />}>
    <div className={styles.loadStack}>
      <SuspenseLoad />
      <SuspenseLoad skip={5} take={5} />
    </div>
  </Suspense>
</div>

In this case we want to load two chunks of data form the server and show them in the stack. Both requests are independent and we can load them in parallel.

It is evident that we can reduce spinner control logic significantly. To have the single-loading spinner with the stack of ClassicLoad components, we need to have a callback for loading events or raise loading logic in the composition component. Otherwise, for SuspenseLoad components, we simply move boundaries for Suspense.

The result of this code is the following:

Stack of components

As we can see, UI is improved a well. We can manage loading state for two components in the single place.

Optimistic UI updates

An optimistic UI update is another great example of using React Suspense with Apollo Client. Let’s write to the cache part of the next query and initiate the request to the backend.

client.writeQuery({
  query: GET_ITEMS,
  variables: { skip: 10, take: 5 },
  data: {
    items: [
      {
        id: '10',
        title: 'Cached data for item 10',
      },
    ],
  },
})

We add already cached data in the third page of the list, and now we can request these data from the server:

<div className={styles.loadContainer}>
  <div className={styles.loadStack}>
    <ClassicLoad skip={10} take={5} />
  </div>
  <Suspense fallback={<Loading />}>
    <div className={styles.loadStack}>
      <SuspenseLoad skip={10} take={5} />
    </div>
  </Suspense>
</div>

And UI looks like this:

Displaying cached data

As we can see, the “classic” approach shows us the waiting spinner when we initiate the request, but the component with Suspense can show us cached data and update the component state when data are ready.

Error handling

Another crucial moment in application development is error handling. With React Suspense, we can pass error handling to the composition component as well. To implement this approach, we need to create a class component (yeah, good ol’ class components).

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props)
    this.state = {
      hasError: false,
      error: undefined,
      errorInfo: undefined,
    }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.setState({ hasError: true, error, errorInfo })
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className={styles.container}>
          <h1>Something went wrong.</h1>
          <div>
            <b>Message</b> {this.state.error?.message}
          </div>
          <div>
            <b>Info</b> {this.state.errorInfo?.digest}
          </div>
          <div>
            <b>Component stack</b> {this.state.errorInfo?.componentStack}
          </div>
        </div>
      )
    }

    return this.props.children
  }
}

And wrap our Suspense with ErrorBoundary.

<div className={styles.loadContainer}>
  <div className={styles.loadStack}>
    <ClassicLoad skip={100500} take={5} />
  </div>
  <ErrorBoundary>
    <Suspense fallback={<Loading />}>
      <div className={styles.loadStack}>
        <SuspenseLoad skip={100500} take={5} />
      </div>
    </Suspense>
  </ErrorBoundary>
</div>

This allows us to deduplicate error handling and make our components even more clean.

function SuspenseLoad({ skip = 0, take = 5 }: SuspendLoadProps) {
  const { data } = useSuspenseQuery(GET_ITEMS, {
    variables: {
      skip,
      take,
    },
    returnPartialData: true,
  })

  return (
    <ul className={styles.container}>
      {data?.items?.map((item) => (
        <li className={styles.listItem} key={item?.id}>
          <span className={`${styles.itemField} ${styles.idField}`}>
            {item?.id}
          </span>
          <span className={styles.itemField}>{item?.title}</span>
        </li>
      ))}
    </ul>
  )
}

Conclusion

In conclusion, I may say that Apollo Client has more exciting features to work with React Suspense. And yes, React Suspense is a powerful feature that should be considered in your production applications if your data source supports internal mechanics.

The full source code of the article and instruction how to run your local test environment may be found here: https://github.com/a13xg0/react-suspence-playground

Useful links: https://www.apollographql.com/docs/react/data/suspense/