Original Article: A Fundamental Guide To React Suspense
Another major feature that will be released in React 18 is Suspense. If you have been in the React development field for a while, you would know that Suspense is not particularly new. It was introduced as an experimental feature in React version 16.6 back in 2018. At that time, it was mainly focused on handling code splitting with React.lazy.
However, with the release of React 18, the official release of Suspense is now upon us. Along with the release of concurrent rendering, the true power of Suspense is finally unleashed. The interaction between Suspense and concurrent rendering opens up a world of opportunities for improving user experience.
But, like all features, just like concurrent rendering, it is important to start with the basic principles. What exactly is Suspense? Why do we need Suspense in the first place? How does Suspense solve this problem? What are the benefits? To help you understand these basic principles, this article will discuss these questions in detail and provide you with a solid knowledge foundation on the topic of Suspense.
What is Suspense#
Essentially, Suspense is a mechanism that allows React developers to indicate to React that a component is waiting for data to be ready. React then knows that it should wait for that data to be fetched. Meanwhile, it displays a fallback to the user and continues rendering the rest of the application. Once the data is ready, React goes back to that specific user interface and updates it accordingly.
Fundamentally, this sounds similar to how React developers currently have to implement the data fetching process: using some kind of state to indicate if the component is still waiting for data, using a useEffect to initiate the data fetching, displaying a loading state based on the status of the data, and updating the UI once the data is ready.
However, in practice, Suspense technically makes this scenario happen in a completely different way. Unlike the mentioned data fetching process, Suspense integrates deeply with React, allowing developers to coordinate the loading state more intuitively and avoiding race conditions. To better understand these details, it is important to know why we need Suspense in the first place.
Why do we need Suspense#
Without Suspense, there are two main approaches to implement data fetching: fetch-on-render and fetch-then-render. However, these traditional data fetching processes come with some issues. To understand Suspense, we must delve into the problems and limitations of these processes.
Fetch-on-render#
Most people would use useEffect and state variables to implement the data fetching process, as mentioned earlier. This means that data fetching only happens when a component is rendered. All data fetching occurs within the effects and lifecycle methods of the component.
The main issue with this approach is that components trigger data fetching only when they are rendered, and their asynchronous nature forces them to wait for other components' data requests.
Let's say we have a ComponentA that fetches some data and has a loading state. Inside, ComponentA also renders another component, ComponentB, which also performs some data fetching. However, due to the way data fetching is implemented, ComponentB only starts fetching data after it is rendered. This means it has to wait until ComponentA finishes fetching data before rendering ComponentB.
This leads to a waterfall-like approach, where data fetching between components happens sequentially, essentially meaning they are 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 avoid the sequential blocking of data fetching between components, an alternative approach is to start all data fetching work as early as possible. Instead of having components handle data fetching during rendering and having data requests happen separately, we start all requests before the tree starts rendering.
The benefit of this approach is that all data requests are initiated together, so Component B doesn't have to wait for Component A to finish. This solves the problem of components blocking each other in terms of data flow. However, it also brings another issue where we have to wait for all data requests to complete before presenting anything to the user. It's not an ideal experience, as you can imagine.
// Start fetching data 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 this problem#
Fundamentally, the main problem with fetch-on-render and fetch-then-render can be summarized by the fact that we are trying to forcefully synchronize two different processes: the data fetching process and the React lifecycle. With Suspense, we get a different approach to data fetching, known as render-as-you-fetch.
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} />
}
Unlike the previous implementations, it allows components to initiate data fetching at the moment React reaches them. This happens even before the component is rendered, and React doesn't stop there. It continues evaluating the subtree of the component and tries to render it while waiting for the data fetching to complete.
This means Suspense doesn't block rendering, so child components don't have to wait for their data fetching requests to be initiated after their parent components. React tries to render as much as possible while initiating the appropriate data fetching requests. Once a request is completed, React revisits the corresponding component and updates the user interface with the newly received data accordingly.
What are the benefits of Suspense#
Suspense has many benefits, especially in terms of user experience. But some of these benefits also cover the developer experience.
-
Early initiation of fetching: The biggest and most immediate benefit of the render-as-you-fetch approach introduced by Suspense is that data fetching is initiated as early as possible. This means shorter waiting times for users and faster application speed, which is universally beneficial for any frontend application.
-
More intuitive loading states: With Suspense, components no longer have to include a bunch of messy if statements or separately track states to implement loading states. Instead, the loading state is integrated into the component itself. This makes components more intuitive as it keeps the loading code together with the relevant code, and since the loading state is contained within the component, it is easier to reuse.
-
Avoidance of race conditions: One issue with existing data fetching implementations, which I didn't delve into in this article, is race conditions. In certain cases, the traditional fetch-on-render and fetch-then-render implementations can lead to race conditions depending on factors like timing, user input, and parameterized data requests. The main potential issue is that we are trying to forcefully synchronize two different processes: React's and data fetching. But with Suspense, this can be integrated more elegantly, avoiding the mentioned problems.
-
More comprehensive error handling: With Suspense, we essentially create boundaries for the data fetching flow. Besides that, since Suspense integrates more intuitively with the component's code, it allows React developers to implement more integrated error handling for both React code and data requests.
Conclusion#
React Suspense has been in the spotlight for over 3 years. But with the release of React 18, the official release is getting closer. It will be one of the biggest features released as part of this React version, following concurrent rendering. On its own, it elevates the implementation of data fetching and loading states to a new level of intuitiveness and elegance.
To help you understand the basic principles of Suspense, this article covered several important questions and aspects about it. This includes what Suspense is, why we need something like Suspense in the first place, how it solves certain data fetching problems, and all the benefits that Suspense brings.