Sharing state between client and server in React

2024-06-25

When using React Server Components, sharing state between the server and the client becomes really useful in many scenarios.

Pagination

Usually we fetch data in a server component and use buttons in a client component to navigate between pages. In order to achieve this, we need to share the current page between the server and the client so that when the client changes the current page, the server can fetch the data for the new page.

The Idea

The idea is very simple, we can use the URL to share the state between the server and the client. The server component reads the shared state from the URL and the client component updates the shared state by updating the URL.

Basic example

Basically we need to do the following:

  1. Create a server component as the root of the movies page
  2. Parse the URL params using zod
  3. Fetch the movies using the page param we obtained from the URL
  4. Render the list of movies
  5. Render a client component that will navigate to the next page of movies
  6. Pass the page param to the client component
// app/page.tsx
import { z } from "zod";
import { getMovies } from "../data/movies";
import MoviesControls from "./_components/MoviesControls";
import MoviesList from "./_components/MoviesList";

export default async function Home({
  searchParams,
}: {
  searchParams?: { [key: string]: string | string[] | undefined },
}) {
  const pageParam = z.coerce.number().catch(1).parse(searchParams?.page);
  const movies = await getMovies({ page: pageParam });
  return (
    <div>
      <h1>Movies</h1>
      <MoviesList movies={movies} />
      <MoviesControls currentPage={pageParam} />
    </div>
  );
}

Note

As you can see, we are using Zod to parse the URL params, we are trying to convery the page param to a number and if it fails we default to 1. Obviously, that's not the most robust way to handle this, but it's good enough for this example.

When the user clicks on the next button, we need to create new URL params with the value of the next page as the page param, after that, we need to perform a client side navigation to the same route but with the new URL params which contain the updated page param.

That will cause Next.js to make a new request to the route with the new URL params and as such the server component will rerender with the new URL params and fetch the moveis for the new page param.

// app/_components/Controls.tsx
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

export default function MoviesControls({ currentPage }: { currentPage: number }) {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const urlSearchParams = new URLSearchParams(searchParams.toString());

  const handleNext = () => {
    const nextPage = currentPage + 1;
    urlSearchParams.set("page", nextPage.toString());

    router.replace(`${pathname}?${urlSearchParams.toString()}`);
  };
  return (
    <div>
      <div>Page: {currentPage}</div>
      <button onClick={handleNext}>Next</button>
    </div>
  );
}

Here's a live example of the code above: CodeSandbox