The New Suspense API in React
Chapter 10 explores the innovative Suspense API in React 18, focusing on its application in asynchronous data fetching and UI streaming. The chapter demonstrates how Suspense, combined with streaming in Next.js, enhances UI interactivity and responsiveness.
In this chapter, you will learn
We've previously explored code splitting and lazy loading using Suspense in Chapter 5. The Suspense API's ability to wait for components to be ready for rendering is impressive, but what if we could apply this pattern to all asynchronous data fetching?
Consider the following scenario: In About
, we fetch data and wrap it in Suspense
. While fetching user details, a skeleton component is displayed, and upon data retrieval, the actual content is rendered.
This approach wasn't feasible until the release of React 18, which expanded the use of Suspense beyond just code splitting.
It's important to note that this new application of Suspense is still experimental and not yet widely considered production-ready. However, libraries like SWR and React Query, and frameworks such as Next.js, are already experimenting with it.
Let's examine how to implement data-fetching with Suspense.
Suspense & Fallback
First, create a UserInfo
component as a wrapper for About
and Friends
:
The UserInfo
component, defined as an asynchronous function, fetches user data based on the given id
. The fetched user data is then passed to the About
component. However, since getUser(id)
is an async call, the entire UserInfo
component, including the About
component, will only render once the user data is successfully fetched.
For the Feeds
component, it's wrapped in a Suspense
component. This means if Feeds
is waiting for its data (like fetching additional information based on the user's interests), the Suspense
component will display the FeedsSkeleton
as a fallback. Once the data required by Feeds
is available, the Feeds
component will render with the actual data.
Therefore, both About
and Feeds
depend on the asynchronous fetching of user data, but Feeds
specifically leverages React's Suspense for a smoother loading experience. The use of Suspense for Feeds
allows for a part of the component tree to wait on its own data fetching independently, providing a fallback during the wait.
Here, we use Suspense for the Feeds
component. The Friends
component is defined as follows:
We define another asynchronous Friends
function component in React, upon receiving a user id
as a prop, the component fetches the user's friends using the getFriends(id)
function. After fetching the friends' data, we map the users into Friend
components.
These components are then utilized in the Profile
container:
Let's visualise the component tree to have a more direct perspective, we'll learn how such boundary can help the performance in the section Streaming in Next.js later.
Using skeletons in different layers
Initially, skeleton components UserInfoSkeleton
and FriendsSkeleton
are displayed. As data fetched inside each component correspondingly, we can then render the component with data.
In this segment of the code, we define two components in React: UserInfoSkeleton
and UserInfo
.
UserInfoSkeleton
is a simple functional component that serves as a placeholder while the actual user information is being fetched. It combines two skeleton components, AboutSkeleton
and FeedsSkeleton
, indicating that the UserInfo
component will consist of two main parts: one for 'About' information and another for 'Feeds'.
The UserInfo
component is more complex. It is an asynchronous function that takes a user id
as a prop and fetches the user's data with getUser(id)
. Once the data is fetched, it renders two components: About
and Feeds
. The About
component is rendered directly with the fetched user data. For the Feeds
component, React's Suspense is used. The Feeds
component is responsible for displaying content based on the user's interests, and while it's fetching this data, the FeedsSkeleton
is shown as a fallback. Once the Feeds
data is ready, the actual Feeds
component replaces the skeleton.
This setup allows for a smooth and progressive loading experience. Initially, the UserInfoSkeleton
is displayed. As data is fetched, the actual components (About
and Feeds
) gradually replace their respective skeleton components. This approach enhances the user experience by providing visual feedback during data loading and progressively revealing content as it becomes available.
Streaming in Next.js
Next.js documentation explains streaming as a technique to progressively render UI from the server. It splits work into chunks streamed to the client as they're ready. This allows for immediate rendering of parts of the page.
In Next.js, streaming can be achieved through:
- A special
loading.tsx
file in your app router. - Using the Suspense API.
We've seen Suspense above. Alternatively, you can define a loading.tsx
in app/user/[id]/loading.tsx
:
In page.tsx
, you can await all data before rendering:
Alternatively, you can push streaming to individual components, allowing earlier user interaction. This is the approach we're using now as it's more flexible, we can define more granular skeleton and suspense boundary, that also means potentially more content can be static thus rendered much faster (think of the headline, section header, etc.).
We aim to shift content generation to the server side as much as possible, but client components remain essential for high interactivity.
Optimizing UI by Grouping Related Data Components
A key strategy to enhance user experience is to group related data components in the UI. This approach ensures that related information is displayed together, aligning with the user's expectations and the logical flow of data. For instance, in our application, user information displayed in the About
component is closely related to the Feeds
, as both pertain to the user's interests. Conversely, the Friends
component, which can function independently, might be more suitably placed in a separate section of the UI.
To implement this, we consider a left-right layout that visually and functionally groups related components together. Such an arrangement not only improves the coherence of the displayed information but also enhances the overall aesthetic and navigability of the UI.
In practice, this layout adjustment requires minimal changes to the code. We introduce a vertical version of the FriendsSkeleton
, named FriendsSkeletonVertical
, to align with the new layout. The Profile
component is then updated to reflect this new structure:
This updated layout ensures that each component manages its own data fetching, maintaining a clean separation of concerns. The use of Suspense boundaries for each component enhances the experience by providing immediate visual feedback (through skeletons) and progressively loading the content. This approach not only streamlines the rendering process but also optimizes the interactivity and responsiveness of the application, ensuring users have a smooth and engaging experience navigating through different sections of the user profile.
You have Completed Chapter 10
In Chapter 10, we investigate the new Suspense API in React 18, highlighting its role in asynchronous data fetching and UI streaming. The chapter provides insights into using this API with Next.js to progressively render content and improve user interaction.
This chapter delves into React 18's new Suspense API, showcasing its capabilities in transforming data fetching and user interface rendering. We explore how to utilize this API alongside Next.js for a more interactive and responsive user experience.