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>
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 usestartTransition
, 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;
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;
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
}
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;
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;
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)
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?
Btw, I did build a simple todo list application, and you can see how the
mutate
in SWR works by looking at this codeI 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
Thanks!
I will check it...
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.
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?
Thanks for the article! It was great help!