Search code examples
javascriptreactjstypescriptnext.jsserver-side-rendering

Server side component list filtering in Next.js


I was used to the previous Next.js architecture and I started a new project where I'm trying to use the new architecture with server and client components.

I have a page that is a race result table that needs to be rendered on the server for SEO purpose. Once the page is loaded, I want to have a filter input field to allow the user to find the athlete they are looking for.

I'm a bit confused about how to do it, and I feel like it's impossible. I tried to use the getServerSideProps function as in the previous architecture, but that is not working on the new one.

Any idea about how to do it ? Here is what my page code actually looks like:

const RaceResultsPage = async ({ params }: { params: { raceId: string } }) => {
    const result = await getResultByRaceId(+params.raceId);

    return (
        <>
            <div>
                {/* The input I want to use to filter the table*/}
                <div className="relative mt-2 rounded-md shadow-sm">
                    <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
                        <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
                    </div>
                    <input
                        type="email"
                        name="email"
                        id="email"
                        className="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                        placeholder="Filtrer par nom, prénom, numéro, club"
                    />
                </div>
            </div>

            <div className="mt-8 flow-root">
                <div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                    <div className="inline-block min-w-full py-2 align-middle">
                        <table className="min-w-full divide-y divide-gray-300">
                            <thead>
                            <tr>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Pos.
                                </th>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Bib.
                                </th>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Nom
                                </th>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Cat
                                </th>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Temps
                                </th>
                                <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
                                    Ecart
                                </th>
                            </tr>
                            </thead>
                            <tbody className="divide-y divide-gray-200 bg-white">
                            {result.map((person) => (
                                <tr key={person.bib}>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.pos}</td>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.bib}</td>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm font-medium text-gray-900">
                                        {person.name}
                                    </td>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.catRank}</td>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.totalTime}</td>
                                    <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{person.diff}</td>
                                </tr>
                            ))}
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </>
    );
};

I'm using Next.js 14.


Solution

  • If you want your page to be a server component, the only way to have a filter is to reload the page with something in the URL, a query string for example.

    Still, to update the URL, you would need a client component while keeping the page itself as a server component.

    For example, you can do so:

    // app/SetQueryFilters.tsx
    
    "use client";
    
    import { usePathname, useRouter, useSearchParams } from "next/navigation";
    import { useCallback } from "react";
    
    export default function SetQueryFilters() {
      const router = useRouter();
      const pathname = usePathname();
      const searchParams = useSearchParams();
    
      const createQueryString = useCallback(
        (name: string, value: string) => {
          const params = new URLSearchParams(searchParams.toString());
          params.set(name, value);
    
          return params.toString();
        },
        [searchParams]
      );
    
      return (
        <>
          <input
            type="text"
            value={searchParams.get("filter") || ""}
            onChange={(e) => {
              router.push(pathname + "?" + createQueryString("filter", e.target.value));
            }}
          />
        </>
      );
    }
    
    // app/page.tsx
    
    import { Suspense } from "react";
    import SetQueryFilters from "./SetQueryFilters";
    
    const RaceResultsPage = async ({
      params,
      searchParams,
    }: {
      params: { raceId: string };
      searchParams: { filter: string };
    }) => {
      const result = await fetch("https://jsonplaceholder.typicode.com/users");
      let data = await result.json();
      data = data.filter((person) => {
        if (searchParams.filter) {
          return person.name.toLowerCase().includes(searchParams.filter.toLowerCase());
        }
        return true;
      });
      return (
        <>
          <Suspense fallback={<div>Loading...</div>}>
            <SetQueryFilters />
          </Suspense>
          <table>
            <thead>
              <tr>
                <th scope="col">Name</th>
                <th scope="col">Username</th>
                <th scope="col">Email</th>
              </tr>
            </thead>
            <tbody>
              {data.map((person) => (
                <tr key={person.id}>
                  <td>{person.name}</td>
                  <td>{person.username}</td>
                  <td>{person.email}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      );
    };
    
    export default RaceResultsPage;