A Fundamental Guide To React Suspense
February 27, 2022 - 7 minutes
Another big feature that will be released in React 18 is Suspense. If you’ve been in the React development field for a longer time, then you’ll know that the Suspense feature isn’t particularly new. Back in 2018, Suspense was released as an experimental feature as part of the React version 16.6. Then, it was mainly targeted towards handling code splitting in combination with React.lazy
.
But now, with React 18, the official release of Suspense is in front of us. Together with the release of concurrent rendering, the real power of Suspense is finally unlocked. The interactions between Suspense and concurrent rendering open up an enormous world of opportunities to improve user experience.
But just like with all features, just like concurrent rendering, it’s important to start with the fundamentals. What is Suspense exactly? Why do we need Suspense in the first place? How does Suspense solve that problem? What are the benefits? To help you with understanding these fundamentals, this article will go over exactly those questions and provide you with a solid foundation of knowledge regarding the topic of Suspense.
What is Suspense?
In essence, Suspense is a mechanism for React developers to indicate towards React that a component is waiting for data to be ready. React then knows that it should wait for that data to be fetched. In the meanwhile, a fallback will be shown to the user and React will continue with rendering the rest of the application. After the data is ready, React will come back to that particular UI and update it accordingly.
Fundamentally, this doesn’t sound too different from the current way in which React developers have to implement data fetching flows: using some kind of state to indicate whether a component is still waiting for data, an useEffect
that starts data fetching, showing a loading state based on the data’s status, and updating the UI after data is ready.
But in practice, Suspense makes this happen in a technically totally different. Contrary to the mentioned data fetching flow, Suspense deeply integrates with React, allows developers to more intuitively orchestrate loading states, and avoids race conditions. To gain a better understanding of those details, it’s important to know why we need Suspense.
Why do we need Suspense?
Without Suspense, there are two main approaches towards implementing data fetching flows: fetch-on-render and fetch-then-render. However, there are some issues with those traditional data fetching flows. To understand Suspense, we have to dive into the problems and limitations of those flows.
Fetch-on-render
Most people will implement data fetching flows as mentioned before, using useEffect
and state variables. This means that data only begins fetching when a component renders. All the data fetching happens in components’ effects and lifecycle methods.
The main issue with this method, where components only trigger data fetching on render, is that the async nature forces components to have to wait for the data requests of other components.
Let’s say we have a component ComponentA
that fetches some data and has a loading state. Internally, ComponentA
also renders another component ComponentB
, which also performs some data fetching on its own. But due to the way data fetching is implemented, ComponentB
only starts fetching its data when it’s rendered. This means that it has to wait until ComponentA
is done fetching its data and then renders ComponentB
.
This results in a waterfall approach where the data fetching between components happen sequentially, which essentially means they’re blocking each other.
function ComponentA() {
const [data, setData] = useState(null);
useEffect(() => {
fetchAwesomeData().then(data => setData(data));
}, []);
if (user === null) {
return <p>Loading data...</p>;
}
return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}
function ComponentB() {
const [data, setData] = useState(null);
useEffect(() => {
fetchGreatData().then(data => setData(data));
}, []);
return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}
Fetch-then-render
To prevent this sequential blocking of data fetching between components, an alternative would be to start all the data fetching as early as possible. So, instead of having components be responsible for handling the data fetching on render and data requests all happen separately, all the requests are initiated before the tree starts rendering.
The advantage of this method is that all data requests are initiated together, and thus ComponentB
doesn’t have to wait for ComponentA
to be done. This solves the issue of components sequentially blocking each other’s data flows. However, it introduces another issue that we have to wait for all data requests to be finished before anything is rendered for the user. As can be imagined, this is not an optimal experience.
// Start fetching before rendering the entire tree
function fetchAllData() {
return Promise.all([
fetchAwesomeData(),
fetchGreatData()
]).then(([awesomeData, greatData]) => ({
awesomeData,
greatData
}))
}
const promise = fetchAllData();
function ComponentA() {
const [awesomeData, setAwesomeData] = useState(null);
const [greatData, setGreatData] = useState(null);
useEffect(() => {
promise.then(({ awesomeData, greatData }) => {
setAwesomeData(awesomeData);
setGreatData(greatData);
});
}, []);
if (user === null) {
return <p>Loading data...</p>;
}
return (
<>
<h1>{data.title}</h1>
<ComponentB />
</>
);
}
function ComponentB({data}) {
return data === null ? <h2>Loading data...</h2> : <SomeComponent data={data} />;
}
How does Suspense solve the data fetching issues?
In essence, the main issue with fetch-on-render and fetch-then-render boils down to the fact that we’re trying to forcefully synchronise two different flows, namely the data fetching flow and the React lifecycle. With Suspense, we arrive at a different kind of data fetching approach, the so-called render-as-you-fetch method.
const specialSuspenseResource = fetchAllDataSuspense();
function App() {
return (
<Suspense fallback={<h1>Loading data...</h1>}>
<ComponentA />
<Suspense fallback={<h2>Loading data...</h2>}>
<ComponentB />
</Suspense>
</Suspense>
);
}
function ComponentA() {
const data = specialSuspenseResource.awesomeData.read();
return <h1>{data.title}</h1>;
}
function ComponentB() {
const data = specialSuspenseResource.greatData.read();
return <SomeComponent data={data} />;
}
The difference with the previous implementations is that it allows components to initiate data fetching the moment React reaches it. This happens even before the component renders and React doesn’t stop there. It then keeps evaluating the subtree of the component and continues trying to render it while waiting for data fetching to be done.
This means that Suspense doesn’t block rendering, which means that subcomponents don’t have to wait for parent components to finish before initiating their data fetching requests. React tries to render as much as possible, all while initiating the appropriate data fetching requests. After a request finishes, React will revisit the corresponding component and update the UI accordingly using the freshly received data.
What are the benefits of Suspense?
There are a lot of benefits that come with Suspense, especially for the user experience. But some of the benefits also cover the developer experience.
- Initiate fetching early. The biggest and most straightforward benefit of the render-as-you-fetch method that Suspense introduces is the fact that data fetching is initiated as early as possible. This means that users have to wait less and that the application is faster, which are universally beneficial to any frontend application.
- More intuitive loading states. With Suspense, components don’t have to include a huge mess of if statements or separately keep track of states anymore to implement loading states. Instead, loading states are integrated into the component itself that it belongs to. This makes the component more intuitive, by keeping the loading code close to the related code, and more reusable, as loading states are included in the component.
- Avoids race conditions. One of the issues with existing data fetching implementations that I didn’t cover in-depth in this article are race conditions. In certain scenarios, the traditional fetch-on-render and fetch-then-render implementations could lead to race conditions depending on different factors like timing, user input, and parameterised data requests. The main underlying issue is that we’re trying to forcefully synchronise two different processes, React’s and data fetching. But with Suspense, this is done more gracefully and integrated, which avoids the mentioned issues.
- More integrated error handling. Using Suspense, we’ve basically created boundaries for data request flows. On top of that, because Suspense makes it integrate more intuitive with the component’s code, it allows React developers to also implement more integrated error handling for both the React code and data requests.
Final Thoughts
React Suspense has been on the radar for more than 3 years. But with React 18, the official release is becoming increasingly close. Next to concurrent rendering, it will be one of the biggest features that will be released as part of this React release. On its own, it could elevate data fetching and loading state implementation towards a new level of intuitiveness and elegance.
To help you understand the fundamentals of Suspense, this article covered several questions and aspects that are important to it. This involved going over what Suspense is, why we needed something like Suspense in the first place, how it solves certain data fetching issues and all the benefits that come with Suspense.