When developing applications, it’s common to encounter components that require substantial client-side functionality, making them ideal candidates for React client components. In this example, we’re working on a dashboard page. For this, we need to fetch data for a UserProfile component (a client-side component) while aiming to avoid the overhead of creating a separate API endpoint.

Fetch the data on the server

One way to tackle this is to fetch the data in the server component and pass it down to the client component. Below is a basic implementation that does just that:

import { Suspense } from 'react';
import { getUser } from './actions/getUser';
export default async function Dashboard() {
  const user = await getUser();
  
  return (
    <p>
      <BigUIChunk />
      <UserProfile user={user} />
    </p>
  );
}

This approach works as expected; however, this introduces a significant issue: the full page render is blocked until the getUser promise is resolved.

The issue with blocking

Blocking the entire page render while waiting for a promise can lead to a subpar experience. The user sees nothing until the server fetch is complete. This could be frustrating, depending on network conditions or backend performance. Users expect dashboards to load immediately, even if some content needs to be fetched in the background.

A non-blocking alternative: Pass the promise to the client

To improve the user experience, we can opt for a non-blocking approach. Instead of resolving the promise on the server, we can pass the unresolved promise down to the client and let it resolve there. This way, the page can render immediately, and the data will be loaded asynchronously.

Here’s how we implement this approach:

import { Suspense } from 'react';
import { getUser } from './actions/getUser';
export default function Dashboard() {
  // Don't resolve the promise
  const getUserPromise = getUser();
  
  return (
    <p>
      <BigUIChunk />
      <Suspense fallback={<p>loading...</p>}>
        <UserProfile getUserPromise={getUserPromise} />
      </Suspense>
    </p>
  );
}

In this version, we wrap the UserProfile component in React’s Suspense component. This allows us to provide a fallback UI, such as a spinner or loading skeleton, while the promise is resolved. This improves perceived performance as the user immediately sees part of the page load while the rest of the content is fetched in the background.

Resolving the promise in the component

Once the promise reaches the client-side component, we can resolve it using React’s use hook. This approach ensures the user data is fetched and used as soon as it becomes available. Here’s how we would handle this in the client component:

"use client";
import { use } from 'react';
export default function UserProfile({ getUserPromise }) {
  const user = use(getUserPromise);
  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

In Conclusion, passing down promises to client components and resolving them using the use hook gives us a flexible way to handle asynchronous data without blocking the initial render. This pattern ensures a smoother user experience on pages where users expect fast, immediate interaction.