10
Chapter 10

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

  • Exploring the new Suspense API for data fetching in React 18
  • Implementing UI streaming with Next.js for dynamic content rendering
  • Enhancing user experience with progressive loading and structured data fetching

  • 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.

    const About = async ({ id }) => { const user = await get(`/users/{id}`); return <div>{user.name}</div>; }; const App = () => { return ( <Suspense fallback={<AboutSkeleton />}> <About id={id} /> </Suspense> ); };

    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:

    components/userinfo.tsx
    export async function UserInfo({ id }: { id: string }) { const user = await getUser(id); return ( <> <About user={user} /> <Suspense fallback={<FeedsSkeleton />}> <Feeds category={user.interests[0]} /> </Suspense> </> ); }

    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:

    components/friends.tsx
    import { Friend } from "@/components/friend"; async function Friends({ id }: { id: string }) { const friends = await getFriends(id); return ( <div className="py-4"> <h2 className="text-lg text-slate-900 tracking-wider">Friends</h2> <div className="flex flex-row pt-4 gap-4"> {friends.map((user) => ( <Friend user={user} key={user.id} /> ))} </div> </div> ); } export { Friends };

    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:

    components/profile.tsx
    import { Suspense } from "react"; import { Friends } from "@/components/v4/friends"; import { UserInfo } from "@/components/v4/userInfo"; import { FriendsSkeleton } from "@/components/misc/friends-skeleton"; import { UserInfoSkeleton } from "@/components/misc/user-info-skeleton"; export async function Profile({ id }: { id: string }) { return ( <div> <h1>Profile</h1> <div> <Suspense fallback={<UserInfoSkeleton />}> <UserInfo id={id} /> </Suspense> <Suspense fallback={<FriendsSkeleton />}> <Friends id={id} /> </Suspense> </div> </div> ); }

    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.

    Suspense boundaries
    Suspense boundaries

    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.

    components/misc/user-info-skeleton.tsx
    const UserInfoSkeleton = () => { return ( <> <AboutSkeleton /> <FeedsSkeleton /> </> ); };

    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'.

    components/userinfo.tsx
    export async function UserInfo({ id }: { id: string }) { const user = await getUser(id); return ( <> <About user={user} /> <Suspense fallback={<FeedsSkeleton />}> <Feeds category={user.interests[0]} /> </Suspense> </> ); }

    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:

    app/user/[id]/loading.tsx
    export default function Loading() { return ( <div> <h1>Profile</h1> <div> <AboutSkeleton /> <FeedsSkeleton /> <FriendsSkeleton /> </div> </div> ); }

    In page.tsx, you can await all data before rendering:

    export default async function Page({ params }: PageProps) { const { user, friends } = await getUserBasicInfo(params.id); return (<Profile user={user} friends={friends} />); }

    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.).

    Partial Rendering with Skeletons
    Partial Rendering with Skeletons

    We aim to shift content generation to the server side as much as possible, but client components remain essential for high interactivity.

    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.

    UI Rearrangement Based on Data Grouping
    UI Rearrangement Based on Data Grouping

    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:

    export async function Profile({ id }: { id: string }) { return ( <div> <h1>Profile</h1> <div> <div> <Suspense fallback={<UserInfoSkeleton />}> <UserInfo id={id} /> </Suspense> </div> <div> <Suspense fallback={<FriendsSkeletonVertical />}> <Friends id={id} /> </Suspense> </div> </div> </div> ); }

    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.

    10

    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.

    © 2023