KendoReactT2_1200x303

Suspense lets you asynchronously load data, or any other code asynchronously, and declaratively specify a loading UI while the user is waiting. In this article, I'm going to focus on using Suspense for data fetching with an example using KendoReact Data Grid.

React 16.6 added a <Suspense> component that lets you asynchronously load components, data or any other code, and declaratively specify a loading UI while the user is waiting. This lets us build better responsive apps with smoother state/page transitions.

In this article, I’m going to focus on using Suspense for data fetching with an example using KendoReact Data Grid.

Data Loading without Suspense

When working with data in React, we often put the code to fetch data alongside the component by calling it inside the componentDidMount method or using the useEffect hook. Let’s look at an example using a KendoReact Data Grid that shows basic user information.

Let’s say you have a function to fetch user data:

export async function getUsers(count = 10) {
  const url = `https://randomuser.me/api/?results=${count}`;

  const response = await fetch(url);
  const data = await response.json();
  return data.results.map(({ name, dob, gender, location }) => ({
    name: `${name.first} ${name.last}`,
    age: dob.age,
    gender: gender,
    country: location.country,
  }));
}

and a component to call this function and display the result in a table:

import { Grid, GridColumn } from "@progress/kendo-react-grid";
import { useState, useEffect } from "react";
import getUsers from "./data/user-service";

export default function GridContainer() {
  const [users, setUsers] = useState(null);

  useEffect(() => {
    const loadData = async () => {
      const data = await getUsers();
      setUsers(data);
    };
    loadData();
  }, []);

  return (
    <Grid data={users}>
      <GridColumn field="name" />
      <GridColumn field="age" />
      <GridColumn field="gender" />
      <GridColumn field="country" />
    </Grid>
  );
}

In this component, I’m using hooks to asynchronously load the data when this component is rendered.

When this component is rendered, it will behave as you see in the screen recording below.

For a moment, the screen says, "No records available", then a list of data appears.

You should notice some seconds delay between showing “no records available” and displaying data. This is a confusing transition especially if the user is on a slow internet connection. This problem is not an unusual one. One way you could solve this is to introduce a loading state.

export default function GridContainer() {
  const [users, setUsers] = useState(null);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const loadData = async () => {
      const data = await getUsers();
      setUsers(data);
      setLoaded(true);
    };
    setTimeout(() => {
      loadData();
    }, 1500);
  }, []);

  return (
    <>
      {loaded ? (
        <Grid data={users}>
          <GridColumn field="name" />
          <GridColumn field="age" />
          <GridColumn field="gender" />
          <GridColumn field="country" />
        </Grid>
      ) : (
        <h2>Loading Data</h2>
      )}
    </>
  );
}

The change we just made was to add a loaded state, update it when the data is ready, and conditionally render a loading status based on the loaded state. This is one of the problems the React team wants to solve with Concurrent Mode and Suspense. Instead of writing extra code to indicate a loading state, you can signal to React that something is loading in the background and that it should suspend and display a temporary UI until the resource is ready to be displayed.

Data Loading with React.Suspense

The pattern you saw from the previous section can be referred to as fetch-on-render. This means that your code starts fetching data needed for that component only after the component is rendered. There are times when this might not be a desirable experience, and because we know what data a route or component needs, we could asynchronously load the data and render the component in parallel. This pattern we can refer to as render-as-you-fetch, and we can achieve this using Suspense.

The way this will work is you wrap your component in <React.Suspense /> and provide a fallback UI to be rendered when its child components are not ready. In the child component that is wrapped with Suspense, you’ll throw a promise that reads (or attempts to read) the data while the fetch is still in progress. When this promise is thrown, React suspends rendering the component and displays the fallback UI you specified. It would retry until the data is ready, and the actual component will be rendered.

We can add another function to fetch data using this pattern as follows:

export const fetchUsers = (count = 10) => {
  return wrapPromise(getUsers(count));
};

// Note: this is a simplified implementation.
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    },
  };
}

The fetchUsers function is used to initiate the fetch and pass the promise to wrapPromise. The wrapPromise function returns an object with a read() method which behaves in the manner React Suspense expects. If the data fetch is still not yet resolved, it throws a promise. If it succeeds, it returns the result; otherwise, it throws an error.

The implementation you see above is similar to what you’ll find in the React docs. It’s a simplified version of how they implemented it in the Relay framework which would serve as a reference implementation for data library authors, but not to be copied and used in production. It’s strongly advised not to use this feature in production. React Query has an implementation that you can check if you want to go further.

Using React.Suspense

Suspense is part of a set of features the React team is building to help React apps stay responsive and gracefully adjust to the user’s device capabilities and network speed. They’re still experimental and are subject to change, but you can try them in an experimental build. You’ll have to install this version to use the features. There are no semantic versioning guarantees for the experimental builds, and APIs may be added, changed or removed with any experimental release.

To install the experimental build, run:

npm install react@experimental react-dom@experimental

In the entry point of your app, where you have something like:

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

Change it to:

ReactDOM.unstable_createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Now you can wrap your component with <Suspense />. You can put the <GridContainer /> inside <Suspense /> and provide a fallback UI.

<Suspense fallback={<h2>Loading container</h2>}>
  <GridContainer resource={fetchUsers()} />
</Suspense>

You passed a resource prop to <GridContainer /> whose value will be the result of calling fetchUsers(). When fetchUsers() is called, it starts fetching the data and returns an object with a read() function to use to get the data.

You now should update the <GridContainer /> to read data using the resource prop passed to it:

export default function GridContainer({ resource }) {
  const users = resource.read();

  return (
    <Grid data={users}>
      <GridColumn field="name" />
      <GridColumn field="age" />
      <GridColumn field="gender" />
      <GridColumn field="country" />
    </Grid>
  );
}

When this code is executed, it calls read() to get the data. If it’s not ready, it throws a promise that causes it to suspend, and React renders the fallback code you specified. It tries again and, if the data is ready, it renders the component and replaces the fallback UI.

At the very beginning, it shows"Loading container" before data shows up.

That is how you can use Suspense for data fetching. What I didn’t show is what happens when an error occurs. You would handle this using React’s Error boundary.

That’s a Wrap

Suspense is still an experimental feature and its API is subject to change. It’s not yet part of a stable React release, but you can try them in an experimental build as I’ve shown in this article. I described how you can use this new mechanism to fetch data in React but it can be used for other async use cases. You can find a sample repo for the code in this article on GitHub.

References


Peter Mbanugo
About the Author

Peter Mbanugo

Peter Mbanugo is a software developer, tech writer, and maker of Hamoni Sync. When he's not building tech products, he spends his time learning and sharing his knowledge on topics related to GraphQL, Offline-First and recently WebAssembly. He's also a contributor to Hoodie and a member of the Offline-First community. You can follow him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.