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. As early as 2018, Suspense was released as an experimental feature as part of React version 16.6. At that time, it was mainly focused on handling code splitting with React.lazy.
But now, with the release of React 18, Suspense is officially presented to 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 huge opportunity to improve user experience.
However, like all features, it is important to start with the basic principles, just like concurrent rendering. What exactly is Suspense? Why do we need Suspense from the beginning? 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. Then React 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 returns 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 whether the component is still waiting for data, 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 situation 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.
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 have 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 problem 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, we also render 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 and rendering separately, we initiate 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's data flow. However, it also brings another issue, which is that we have to wait for all data requests to complete before presenting anything to the user. It can be imagined that this is not an optimal experience.
// 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 as 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 called 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 even happens 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, which implies that 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 direct 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 time for users and faster application speed, which is universally beneficial for any frontend application.
-
More intuitive loading states. With Suspense, components no longer need 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 the component more intuitive as it keeps the loading code together with the relevant code, and it is easier to reuse since the loading state is contained within the component.
-
Avoidance of race conditions. One issue with existing data fetching implementations, which I haven't delved 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 problem is that we are trying to forcefully synchronize two different processes: React's and data fetching's. But with Suspense, this can be integrated more elegantly, avoiding the aforementioned issues.
-
More comprehensive error handling. With Suspense, we essentially create boundaries for the data request 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, its 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 covers several important questions and aspects of it. This involves 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.