React v18 features: Suspense

Introduction to React v18 Suspense and Render-as-You-Fetch approach

In this lesson, we are going to explore React’s new ”Suspense” API released in v18 and how concurrent features work.

Uday Hiwarale
JsPoint
Published in
17 min readApr 20, 2022

--

(source: unsplash.com)

Loading data has always been a hassle in web development, whether you are working with React or any other framework. The problem arises when you have multiple API calls to fetch data and you need to show the user the loading state of the application. Let’s take a small example.

(source: https://codesandbox.io/s/react-suspense-normal-fetch-parallel-5kiiuh)

In the example above, we have a small application in which we display the User and Post data by making external Rest API calls individually from the components responsible to display them which are User and Post.

We have added a manual delay in the API response but in the real world, this can be a pretty usual behavior. The /users/ call resolves in 5s while /posts/ call resolves in 3s, a little earlier than the /users call. Until the request resolves, they display a Loading... message on the screen.

Since these requests run in parallel, you can’t really guess which one will resolve first. In our case, we see Post earlier than the User and perhaps that would not be a very good experience. If we have a complex application with multiple parallel requests, we would see different parts of the application load in random order. The more natural behavior for an application is to render things from top to bottom.

To fix this issue, we can use a waterfall pattern. What we could do is to nest the Post component inside User such that Post only appears when User is finished loading its data. It would look like the below.

(source: https://codesandbox.io/s/react-suspense-normal-fetch-waterfall-vyswny)

The only thing we modified above is the position of the Post component where it renders. Instead of it being rendered inside the App component, now it renders inside the User component, once the /users/ API call is resolved.

But now we can see another issue. The Post component doesn’t start fetching its data until /users/ API call is resolved. This is by design but now the total loading time of the application is 3s + 5s. This is one of the problems with waterfall data fetching patterns apart from the obvious complexity of the code it introduces.

In both cases, we have something to lose and these problems could have been easily solved if the App component could have managed the orchestration of its children. Well, that can be managed if App component itself fetches the data for User and Post component and decide when to render them.

(source: https://codesandbox.io/s/react-suspense-normal-fetch-parent-state-management-emg2rq)

In the above example, we have moved the data fetching logic to the App component. Once the /users/ and /posts/ calls are resolved, the App component renders the User and Post component and provides the necessary data as props. Until then, it displays a loading state. Since it loads the data in parallel, we don’t have 3s + 5s delay anymore.

Though this seems like an elegant solution on the surface, it’s quite difficult to manage. A component should be self-sufficient to fetch the data on its own, which now is not the case. Also, the loading state management is still very imperative. If you throw one more API call in there, then you would need to adapt the logic of displaying the loading state accordingly. It gets worse if the new API call doesn’t need to block the User and Post component and show them as soon as possible while having a separate loading state for itself.

React v18 to the rescue

Not a few days ago, we got blessed with the new major release of React. React v18 comes with tons of improvements, especially in the user experience department. The whole release looks like it was purposely designed to solve a lot of user experience issues we were complaining about React.

First, let me provide you with some useful links. We are going to talk about this release in a little bit but I can’t just copy-paste the documentation. So whenever you are done reading this article, use the following links to connect the missing links, if you know what I mean.

React v18 doesn’t provide developers with a lot of new features but it improves upon what goes under the hood. But don’t be disappointed, we are not talking about v17, v18 does something impressive that might change how your users experience your application.

The features released in v18 seem to linger around the word “concurrency” and you could see that from the links I shared above. In a nutshell, it means that React can run multiple render processes (updating DOM) in the background and prefer one over the other based on its importance.

For example, when the user types in an input element, which changes the component state, which leads to modifying the DOM (value of the input), which leads to the user seeing the typed character in the input, would be more important than rendering a huge list of objects that input value change might have fetched from the network (like Google search suggestions).

React categories renders into two categories, urgent and transition. The transition renders can be interrupted and React takes care of that mechanism of that under the hood. This happens because an urgent render suddenly gets queued. For example, if React is rendering a huge list of objects while the user types a character in the input element, the rendering of the list will be paused, and the render of the input element will be prioritized. Once the update of the input has been rendered, React will continue to render the list.

💡 I think above explaination should suffice for the scope of this article. But if you want to get in depth idea of the concurrent renderer, please read this section of tge React 18 release notes.

React v18 brings a new built-in component, Suspense. Well, that’s not exactly true. This component is available since v16 and it was used in conjugation with React.lazy. React.lazy is primarily used in code splitting tools to fetch React components asynchronously on demand. If you didn’t know that already, let’s have a quick look.

(source: https://codesandbox.io/s/react-suspense-lazy-load-wgnrjm)

In the example above, we have a User component that we are loading using React.lazy which would asynchronously fetch the React component code with the help of import function. Whenever we place the User component in the code, we wrap it up with the Suspense component provided by React.

<Suspense fallback={<p>Loading...</p>}>
<User name="John Doe" />
</Suspense>

Now here is some terminology to learn. A component may suspend (dictionary definition) its rendering whenever it’s doing something that needs to be there to render the component such as fetching the data from the network. The component may not want to get displayed until it fetched the data from the network. Once the data is ready (or a network error occurs), then only it may want to display useful information to the user. This also means that it won’t have its own loading state.

Whenever Suspense has one or more suspending components in its child tree, no matter where it is in the tree, it will display a fallback component using the fallback prop. When all of the suspending components inside its child tree stop suspending, the whole child tree is rendered. If one of the child components is suspended again (re-suspends), again the fallback component will be shown.

In React v16, Suspense could only work with the React.lazy. Components created with React.lazy suspends by default. So in the above example, until ./User.tsx file is downloaded by import(), the User component suspends which is caught by the Suspense component and the fallback component (which is the loading indicator here) is shown.

In React v18, React team has increased the capability of the Suspense component and now it can be more generalized. Now a component has the freedom to choose when it can suspend such as while fetching the data from the network. So if a component is fetching the data, it suspends and a fallback loading component can be displayed until the component is ready to display the data it has fetched from the network.

How a component suspends, that logic is hidden and not yet exposed to the developers. In the RFC 0213 document, React has clearly stated it in their unresolved questions

The exact protocol a component should use to tell React that it’s suspended.

So does that means even though we have a more capable Suspense component in v18 but we can’t use it? Yes, of course, we can. It would be a waste of effort from React’s side if we couldn’t do that. However, you may need to use a library that helps your component suspend itself. This is the reason we would be using useSWR for data fetching because it has built-in logic to suspend a component until a network response has been received.

💡 React is working closely with library authors to ship concurrent features so that we developers do not need to worry about under the hood logic. They have already shipped Suspense functionality in their GraphQL library Relay and it soon will be supported in Apollo GraphQL client as well.

Enough talking, let’s look at an example. Before we start using Suspense or any concurrent features of v18, we need to first enable them. We do that by creating a root as shown below.

(source: https://gist.github.com/thatisuday/3f990e2133ced770369250cd3499368f)

You can take a look at this discussion about why we now have to use createRoot to render the application component. But in a nutshell, this method gives us a clean way to manage roots and enables concurrent features of v18 at the same time.

💡 If you upgrade to v18 of React without implementing this method, your application won’t break. The old ReactDOM.render() still supported but it won’t enable concurrent features. This is a “gradual adoption” strategy implemented by the React team so that we can get benefit of new features without making the upgrade backward incompatible.

Now let’s modify our earlier example of fetching /users/ and /posts/ data and displaying it to the user. But this time, we will use Suspense to display a loading state.

(source: https://codesandbox.io/s/react-suspense-swr-1jpw8s)
<Suspense fallback={<p>Loading application...</p>}>
<User id="1" delay={5000} />
<Post id="1" delay={3000} />
</Suspense>

In the above example, we have wrapped the User and Post components under Suspense with a fallback loading component. Now if the User or Post component suspends, the fallback loading state will be displayed. Once all of the suspending components (in our case we have two) stop suspending, then only React will render the children of Suspense.

The reason why User and Post components suspend is that we have used useSWR with suspense option set to true.

const { data, error } = useSWR(key, fetcher, {
suspense: true
});

When React starts rendering User and Post component, it runs the useSWR hook. This hook sends the request to fetch the data from an external API and immediately suspends the component. React stops the rendering of these components until they stop suspending. Once the network response have received, useSWR stops suspending them and the data and error fields will be populated. Now, react can start rendering it again.

(Chrome DevTools Networks Tab)

If you open the Networks panel of your DevTools, you will see two requests have been sent in parallel because React is rendering the User and the Post components. Only after the last request has been resolved, then only see the fallback loading getting removed and actual children being shown.

This seems like a synchronous code that blocks the main thread while it’s fetching the data but in reality, it’s not. React is taking care of a facade that helps us write code in synchronous fashion without it being synchronous. Remember earlier we talked about urgent and transition render. This is another example of a transition render where React pauses the render of the suspending component and prioritizes urgent renders until the component stops suspending

This is the Render-as-you-fetch approach when you are rendering a component as you are fetching the data. The rendering starts as soon as the component mounts but it can be paused by React while in the background it’s fetching the data and once the data arrived, it can resume rendering again.

Let’s try to nest User and Post components inside other components and see how that affects the behavior of Suspense.

(source: https://codesandbox.io/s/react-suspense-swr-nested-2diurs)
<Suspense fallback={<p>Loading application...</p>}>
<UserWrapper>
<User id="2" delay={5000}/>
</UserWrapper>
<div>
<PostContainer />
</div>
</Suspense>

In this example, if we look at the direct children of the Suspense component, none of the components suspends. The UserWrapper component does not suspense but it has User component in its child tree that suspends. Same goes with div and PostContainer component but PostContainer renders the Post component that suspends.

Remember we talked about that Suspense will display the fallback component if any of the components in its children tree suspends. This is what happening here. Since User component stops suspending after 5s, 2s after the Post component, Suspense will display the entire child tree after 5s. You can again verify this by looking at the Networks tab.

(Chrome DevTools Networks Tab)

So what are the benefits here?

  • First of all, it’s awesome that you could write asynchronous code in synchronous way (similar toasync/await here). React uses microtasks to make this happen. Since microtasks are not supported in Internet Explorer, React v18 has dropped support for it (ref).
  • You can dump any suspending component inside a parent component (with Suspense boundary) without worrying about the loading state management for that specific component.
  • You can fetch data in parallel without worrying about the order of appearance of the components.

But you may argue, what if I have a component in the child tree of Suspense that takes a long time to fetch the data and may block the rendering of the important components. This is where we should talk about the term Suspense boundary. Basically, the closest <Suspense> component in the parent tree of a suspending component is called the Suspense boundary.

When a component suspends, the closest <Suspense> component in its parent tree catches it. Catches what again? Well if we think suspending a component is linking throwing an error, then the latest catch block would catch it, no matter how deep inside the code the throw happened as shown below.

(source: https://gist.github.com/thatisuday/6b2736545e8ab81bc2dfc93e0e7e7553)

Since calling c() lead to an exception (even though the exception happened inside a()), it was caught by child catch block and parent catch block remain unaffected as the error did not propagate upwards from there. The same thing happens with Suspense. Think Suspense as a catch block that catches suspending components inside it and does not let it propagate upwards. If there is a parent Suspense above it, it remains unaffected. In a nutshell, Suspense component itself is not suspending. Let’s have a look.

(source: https://codesandbox.io/s/react-suspense-swr-suspense-boundary-lb9uve)

In the example above, instead of putting Photo component that suspends for 10s inside the outer Suspense component, we have wrapped up with another Suspense with its own fallback loader.

<Suspense fallback={<p>Loading application...</p>}>
<User id="1" delay={5000} />
<Post id="1" delay={3000} />
<Suspense fallback={<p>Loading photo...</p>}>
<Photo id="1" delay={10000} />
</Suspense>
</Suspense>

With this, Photo component doesn’t block outer Suspense anymore. The outer suspense renders User, Post and whatever innerSuspense wants to render. Since only User and Post components suspend, the outer Suspense display the child tree as soon as User and Post components stop suspending, regardless of whether Photo component is suspending or not. This is because the inner Suspense catches Photo component’s suspension and display the fallback without blocking the parent Suspense.

(Chrome DevTools Networks Tab)

Now again if you open the Networks tab of your DevTools, you would see three network requests sent in parallel. This is because React is rendering the three components that each first sends a network request and then suspends, and when this happens, React intervenes and pauses the render until the components stop suspending.

Now let’s take a look at what happens when a component re-suspends. A component might re-suspend when it needs to fetch data again and does not want to render until the data is fetched.

(source: https://codesandbox.io/s/react-suspense-swr-re-suspend-7r1zwl)

In the above example, component App stores the ID of the photo to load in its state and by default it is "1". This is sent to the Photo component as the id prop which then the Photo component uses to fetch the data using useSWR.

<Suspense fallback={<p>Loading application...</p>}>
<User id="1" delay={5000} />
<Post id="1" delay={3000} />
<Suspense fallback={<p>Loading photo...</p>}>
<Photo id={photoId} delay={10000} />
</Suspense>
<button onClick={handlePhotoIdChange}>Load New Photo</button>
</Suspense>

When we click on the “Load New Photo” button, it changes to "2" and the Photo component re-renders (because prop has changed). The useSWR hook re-suspends when one of its dependencies changes (and id prop is one of them) and hence we get the “Loading photo” loading state. Since the suspense boundary of Photo is the child Suspense component, we don’t see the loading state of the whole application.

If Photo component suspends again while React was rendering it, React will throw away the previous rendering state and render the entire Suspense tree from the scratch when component stops suspending. This is how React can make sure that Suspense trees are always consistent. You can verify this by clicking on Load New Photo button multiple times and observing the network requests. You will find that only the latest network response is used to render the tree as shown below.

(Chrome DevTools Networks Tab)

But as you can see, having a loading state for 10s perhaps is not a good experience. When the user clicks on the “Load New Photo” button, he/she might want to start loading the new photo in the background while keep interacting with the old photo. When a component suspends inside Suspense caused by a “transition” update, the Suspense boundary will not display the fallback and the user can still interact with the old UI while in the background React is rendering the new UI. Once the new UI is finished (when component stops suspending), it will display the new UI when rendering is finished.

(source: https://codesandbox.io/s/react-suspense-swr-re-suspend-transition-tkbqo1)

To tell React that a state update is a transition update, we use the new useTransition hook. This hook returns two values, just like useState hook.

const [isPending, startTransition] = useTransition();

The first value is a boolean that represents whether a transition update is in flight and the default value is false. The second value is a function that we need to call and provide a callback function as an argument that triggers one or more state updates. These state updates will be considered non-important or transition and React can interrupt the renders of the components that it may cause. In the above example, we have used it like below.

const [isPending, startTransition] = useTransition();const handlePhotoChange = () => startTransition(() => {
const newPhotoId = Number(photoId) + 1;
setPhotoId(`${newPhotoId}`);
});
<Suspense fallback={<p>Loading application...</p>}>
<User id="1" delay={5000} />
<Post id="1" delay={3000} />
<Suspense fallback={<p>Loading photo with id {photoId}...</p>}>
<Photo id={photoId} delay={10000} />
</Suspense>
<button onClick={handlePhotoIdChange}>
{isPending
? `Loading New Photo with id ${photoId}`
: "Load New Photo"}
</button>
</Suspense>

Here, the setPhotoId() call happens inside startTransition which makes it a non-urgent or transition update. Since that makes the Photo component suspend again, Suspense will not show the fallback UI however React will start rendering it in the background. We are using the isPending value to show the user that React is rendering the component in the background so that it doesn’t give the user feeling that button press has done nothing.

💡 If you wish, you can use startTransition function without using the useTransition hook. React provides us the startTransition function in its high level APIs. The startTransition API can be quite useful not just in case of data loading but tab switching and routing. I am going to write a separate article for it and will make sure to put a link to that here.

However, if you click on the “Load New Photo” button multiple times, you can see that in the networks panel, no new requests are sent. Also, the UI doesn’t update the value of photoId. So what’s happening here? Remember that any state updates that are triggered inside startTransition are non-urgent, so React may not consider them to be displayed on the UI immediately.

When photoId is changed, it causes the re-render of Photo component but it suspends. So React will delay the render of photoId until Photo is ready to render to make things consistent. So whenever we access the photoId, it will return the old value since React hasn’t updated it yet. This means we have been overriding the value of photoId using setPhotoId again and again. This prevents us from loading a new photo if the transition is pending.

To fix this, we need to separate out what’s urgent and what’s not urgent. We can see that photoId need to be urgent because we need to get immediate feedback when it’s changed so that we display it to the user. But any effects it may cause don’t need to be urgent, so we need to create another state update from it that’s not urgent.

const [photoId, setPhotoId] = useState("1");
const [loadingPhotoId, setLoadingPhotoId] = useState(photoId);
const [isPending, startTransition] = useTransition();const handlePhotoIdChange = () => {
const newPhotoId = Number(photoId) + 1;
setPhotoId(`${newPhotoId}`);
startTransition(() => {
setLoadingPhotoId(`${newPhotoId}`);
});
};
<Suspense fallback={<p>Loading photo with id {photoId}...</p>}
<Photo id={loadingPhotoId} delay={10000} />
</Suspense>

What we have done above is pretty straightforward. Now we have two states to manage photoId, one is urgent while another one is not. When handlePhotoIdChange() is called, it accesses the photoId state and it’s guaranteed to be up to date as we never update it using startTransition, so the setPhotoId will always increment photoId value consistently.

The handlePhotoIdChange() call also updates the value loadingPhotoId which is the same as the newphotoId. However, the loadingPhotoId value doesn’t need to be immediately rendered so we wrapped it inside startTransition. So internally, every handlePhotoIdChange() call updates the value loadingPhotoId which causes the render of Photo component which in turn fetches the new photo. However, since we are not utilizing the value of loadingPhotoId for any display purpose, it all works out as you can see in the example below.

(source: https://codesandbox.io/s/react-suspense-swr-re-suspend-transition-fixed-cugvoh)

If you have managed to keep up with me this far, then you know how suspense works and you can start using it in your application. Since React doesn’t yet clearly explain the “exact protocol a component should use to tell React that it’s suspended”, we need to rely on libraries like useSWR to handle that behavior for us. But React may give us a high-level API or perhaps a hook in the near future to make this behavior more accessible to developers.

💡 React is also working on the SuspenseList component to orchestrate the revelation of the suspending components with a Suspense boundary. For example, you may want to display components revealed in a forward or backward direction as they unsuspend. It can also give control over the loading states of the Suspense components.

Meanwhile, we should see popular libraries make use of Suspense and other concurrent features for things like routing, data fetching, loading images, loading code, etc. If you are a Next.js developer, then you can start using some of these React features now (documentation). Nevertheless, I feel this whole release has some quite exciting features and a lot can be improved in our application by using them.

(Kausa Berlin officekausa.ai/about)

At Kausa, we always keep ourselves up to date with new features, new technologies, and new paradigms to provide our users with the best user experience possible. I am currently working on migrating our Next.js application to React v18. To know more about the company I work for and if you would like to work with me, please visit Kausa Careers page. And as always, if you have any questions, please write them in the comments.

If you want to get a general overview of features released in React v18, then this article by Shruti Kapoor is highly recommended. If you would like to see this in a video format, then she also has a video on YouTube explaining exactly this presented in React Conf 2021. We will cover the features of React v18 in separate articles in great detail.

--

--