When using React Server Components, sharing state between the server and the client becomes really useful in many scenarios.
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 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.
Basically we need to do the following:
page
param we obtained from the URLpage
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>
);
}
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