Responsive RSC

Instant updates with client-side cached RSCs and Suspense fallbacks

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.

Lets 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. The filter buttons use useTransition to show a spinner while updating search parameters - So in this demo the spinner in the button will be shown for at least 1 second (page delay) and the component skeleton will be shown for at least 2 seconds (server component delay).

Loading

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, previously fetched data would show instantly from local cache. So, we need something like a local cache for Server Components - If user requests the same data again and if we already have the result already requested, we want to stop the additional request to the server and return the result from the local cache.

The Solution: Responsive RSC

responsive-rsc brings client-side caching to Server Components. When you revisit a search param value, the cached RSC renders instantly without a server request.

Loading

Click A, B, C, then click A again - it loads instantly from local cache - No server request is made and UI updates are instant!

How to use responsive-rsc

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>;
}