responsive-rsc

Client-side caching for React Server Components

The Problem

React Server Components can only be updated by changing their props. In Next.js, this typically means updating the URL search params and passing the new values to the component.

Let's look at the below setup where we have a filter component - with values A, B, C. Its currently set to value A. When user changes the value to B, it updates the "value" search parameter of the page.

import { Filter, ComponentSkeleton } from "./client";

async function Component(props) {
  await waitSeconds(2);
  return <div> {props.value} </div>;
}

export default async function Page(props) {
  const searchParams = await props.searchParams;
  const value = searchParams.value || "A";
  await waitSeconds(1);

  return (
    <div>
      <Filter value={value} />
      <Suspense fallback={<ComponentSkeleton />}>
        <Component value={value} />
      </Suspense>
    </div>
  );
}

In the demo, the page component has an artificial delay of 1 second, and the server component has an artificial delay of 2 seconds.

/example

A

You'll also notice that the loading indicator takes about a second to show up in the component - because of the 1 second artificial delay at the page level, this is not ideal user experience.

Click "B", wait for it to load, then click "A" again. Even though "A" was already loaded, you have to wait for the server again. Even if we use caching in the server component with something like unstable_cache, a request to the server will still be made, the page delay will still be present.

With client-side data fetching libraries like React Query, you would get a loading indicator instantly and previously fetched data would show instantly from local cache without making additional requests to the server. If we use server components normally, we don't get these benefits

The Solution

responsive-rsc brings instant loading indicators and client-side caching to Server Components. When you request search parameters for which we already have cached results, the cached Server Components are rendered instantly without a server request

/example

A

Click through the buttons and you'll notice that the component instantly shows the loading indicator unlike the previous demo where it takes about a second to show the loading indicator.

But most importantly, when you click A, B, C, then click A again - it loads instantly from local cache - No server request is made and UI updates are instant - Now that is responsive!

API

Wrap your page with ResponsiveSearchParamsProvider and replace Suspense with ResponsiveSuspense for the server components that rely on search params. Specify which search params the component uses with the searchParamsUsed prop.

import {
  ResponsiveSearchParamsProvider,
  ResponsiveSuspense,
} from "responsive-rsc";
import { Filter } from "./client";

export default async function Page(props) {
  const searchParams = await props.searchParams;

  return (
    <ResponsiveSearchParamsProvider
      value={{ value: searchParams.value }}
    >
      <Filter />
      <ResponsiveSuspense
        searchParamsUsed={["value"]}
        fallback={<SlowComponentSkeleton />}
      >
        <SlowComponent value={searchParams.value} />
      </ResponsiveSuspense>
    </ResponsiveSearchParamsProvider>
  );
}

In client components (like the filter buttons), use useResponsiveSearchParams to read the search parameters and useSetResponsiveSearchParams to update them.

"use client";

import {
  useResponsiveSearchParams,
  useSetResponsiveSearchParams,
} from "responsive-rsc";

export function Filter() {
  const { value } = useResponsiveSearchParams();
  const setResponsiveSearchParams =
    useSetResponsiveSearchParams();

  function handleUpdate(newValue: string) {
    setResponsiveSearchParams((v) => ({
      ...v,
      value: newValue,
    }));
  }

  return <div> ... </div>;
}