DEV Community

Alessio Michelini
Alessio Michelini

Posted on • Edited on

A practical example of Suspense in React 18

The recent release of React 18 brought a lot of changes, nothing that will break the code you already written, but a lot of improvements, and some new concepts.
It also made realize a lot of devs, including me, that we used the useEffect hook the wrong way.
But in our defence we got tricked by the name, as useEffect shouldn't really be used for effects (as this video explains).
In React 18, while you can still use useEffect to do things like populating your state with data you read from an API endpoint, they made it clear that we shouldn't really use it for that purpose, and in fact if you enable StrictMode in your application, in development mode you will find out that using useEffect to will be invoked twice, because now React will mount your component, dismount, and then mount it again, to check if your code is working properly.

Here comes Suspense

What we should use instead is the new component Suspense (well, it was already present in React 17, but now it's the recommended way), and the component will work like this:

<Suspense fallback={<p>Loading...</p>}>
  <MyComponent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

The code above wraps a component, which is loading the data from some datasource, and it will show a fallback until the data fetching is complete.

What it is?

In short, not what you think.
In fact, it is not a new interface to fetch data, as that job is still delegated to libraries like fetch or axios, but instead it lets you integrate those libraries with React, and it's real job is to just say "show this code while is loading, and show that when it's done", nothing more than that.

But how does it work?

Well, first you need to understand how a Promise work, and what are its states.
Regardless of how you consume a promise, if you use the traditional new Promise() or the new async/await syntax, a promise has always these three states:

  • pending -> It's still processing the request
  • resolved-> The request has returned some data and we got a 200 OK status
  • rejected -> Something went wrong and we got an error

The logic used by Suspense is literally the opposite of ErrorBoundary, so if my code is throwing an exception, because it's either still loading or because it failed, show the fallback, if instead it did resolve successfully, show the children components.

Let's see a practica example

Here I'm going to show a simple example, where we are simply going to have a component that needs to fetch some data from an API, and we just want to render our component once it's ready.

Note
For simplicity's sake, I'm going to try to keep thing simple, I'm not going to mention how to use startTransition, or adding Error Boundary, or even the difference between the various strategies like "fetch-on-render", "fetch-then-render", etc... This article is already long enough, but if you want to know about those, please read this article.

Wrap your fetching logic!

As we said above, we need to throw an exception when our components is loading the data or it failed, but then simply return the response once the promise is resolved successfully.
To do that we'll need to wrap our request with this function:

// wrapPromise.js
/**
 * Wraps a promise so it can be used with React Suspense
 * @param {Promise} promise The promise to process
 * @returns {Object} A response object compatible with Suspense
 */
function wrapPromise(promise) {
  let status = 'pending';
  let response;

  const suspender = promise.then(
    res => {
      status = 'success';
      response = res;
    },
    err => {
      status = 'error';
      response = err;
    },
  );

  const handler = {
    pending: () => {
      throw suspender;
    },
    error: () => {
      throw response;
    },
    default: () => response,
  };

  const read = () => {
    const result = handler[status] ? handler[status]() : handler.default();
    return result;
  };

  return { read };
}

export default wrapPromise;
Enter fullscreen mode Exit fullscreen mode

So the code above will check our promise's state, then return a function called read which we'll invoke later in the component.

Now we'll need to wrap our fetching library with it, in my case axios, in a very simple function:

//fetchData.js
import axios from 'axios';
import wrapPromise from './wrapPromise';

/**
 * Wrap Axios Request with the wrapPromise function
 * @param {string} url Url to fetch
 * @returns {Promise} A wrapped promise
 */
function fetchData(url) {
  const promise = axios.get(url).then(({data}) => data);

  return wrapPromise(promise);
}

export default fetchData;
Enter fullscreen mode Exit fullscreen mode

The above is just an abstraction of our fetching library, and I want to stress that this is just a very simple implementation, all the code above can be extended to whatever you need to do with your data. I'm using axios here, but you could use anything you like.

Read the data in the component

Once everything is wrapped up on the fetching side of things, we want to use it in our component!
So, let's say we have a simple component that just read a list of names from some endpoint, and we print them as a list.
And unlike how we did in the past, where we call the fetching inside the component in a useEffect hook, with something that it will look like this example, this time we want to call the request, using the read method we exported in the wrapper, right at the beginning of the component, outside any hooks, so our Names component will start like this:

// names.jsx
import React from 'react';
import fetchData from '../../api/fetchData.js';

const resource = fetchData('/sample.json');
const Names = () => {
  const namesList = resource.read();

  // rest of the code
}
Enter fullscreen mode Exit fullscreen mode

What is happening here, is when we call the component, the read() function will start to throw exceptions until it's fully resolved, and when that happen it will continue with the rest of the code, in our case to render it.
So the full code for that component will be like this:

// names.jsx
import React from 'react';
import fetchData from '../../api/fetchData.js';

const resource = fetchData('/sample.json');

const Names = () => {
  const namesList = resource.read();

  return (
    <div>
      <h2>List of names</h2>
      <ul>
        {namesList.map(item => (
          <li key={item.id}>
            {item.name}
          </li>))}
      </ul>
    </div>
  );
};

export default Names;
Enter fullscreen mode Exit fullscreen mode

The parent component

Now is here were Suspense will come into play, in the parent component, and the very first thing to do is import it:

// parent.jsx
import React, { Suspense } from 'react';
import Names from './names';

const Home = () => (
  <div>
    <Suspense fallback={<p>Loading...</p>}>
      <Names />
    </Suspense>
  </div>
);

export default Home;
Enter fullscreen mode Exit fullscreen mode

So what's happening there?
We imported Suspense as a react component, then we use to wrap our component that is fetching the data, and until that data is resolved, it will just render the fallback component, so just the <p>Loading...</p>, and you can replace with your custom component if you wish so.

Conclusions

After a long time using useEffect for achieving the same results, I was a bit skeptical of this new approach when I first saw it, and the whole wrapping of fetching library was a bit off-putting to be honest. But now I can see the benefits of it, and it makes very easy to handle loading states, it abstract some code away which it makes easier to reuse and it simplify the code of the component itself by getting rid (well, in most of the cases at least) the useEffect hook, which gave me a few headaches in the past.
I also recommend to watch this video from @jherr which really helped me understanding the concept.

Btw, if you want to see how to achieve the same results with less code thanks to SWR, please also read this article.

Top comments (7)

Collapse
 
matheus6_6 profile image
Matheus Barbosa

Firt of all, thanks for the great article!
I am wondering if above the Names component you have an AddName component, with an input and add button, that adds the new name using the "API". What would be the best way to re-render Names component, since we have added a new name?

Collapse
 
darkmavis1980 profile image
Alessio Michelini

Btw, I did build a simple todo list application, and you can see how the mutate in SWR works by looking at this code

Collapse
 
darkmavis1980 profile image
Alessio Michelini

I don't have it, but that's a good suggestion!
What I would recomment to look, is the follow up article I wrote about SWR which will show you how to simplify the code above, and then you can also take a look at mutations with the SWR hook, which you can read on the official docs

Collapse
 
matheus6_6 profile image
Matheus Barbosa

Thanks!

I will check it...

Collapse
 
poc7667 profile image
Poc

Thanks for your sharing, but this example might not be working in real world.
Let's say you have two pages switch back and forth.
The second time you return the page, the global var doesn't get cleared, meaning it's cached and not trigger a data fetch again.

Collapse
 
woodytang profile image
woodytang

the tricky part is const resource = fetchData('/sample.json');

why is it defined as local module variables?

what is exactly this resource?

if I put resource inside the Component, it will be called over and over again, why would this happen?

Collapse
 
aarads profile image
aarads21

Thanks for the article! It was great help!