ProNextJS
    Loading
    lesson

    Naming and Organizing Server and Client Components

    Jack HerringtonJack Herrington

    The App Router in Next.js brought significant changes to how we build applications. We're seeing new patterns emerge, especially in structuring our apps and naming components.

    Let's explore one such pattern focused on server and client component naming and organization.

    The Pokemon List Example

    Here we have a Pokemon list application. It displays a paginated list of Pokemon, allowing users to navigate through pages:

    the pokemon list app

    The code for this demo is inside of the client-server-component directory.

    Examining the code, we see two components working together.

    The PokemonList.tsx files is a React Server Component. This component fetches the initial list of Pokemon from the API and provides a server action to load more data as the user paginates:

    // inside PokemonList.tsx
    import PokemonListClient from "./PokemonListClient";
    
    export default async function PokemonList() {
      const res = await fetch("https://pokeapi.co/api/v2/pokemon");
      const data = await res.json();
    
      async function getPage(page: number) {
        "use server";
        const res = await fetch(
          `https://pokeapi.co/api/v2/pokemon?offset=${page * 20}`
        );
        const data = await res.json();
        return data.results;
      }
    
      return <PokemonListClient pokemon={data.results} getPage={getPage} />;
    }
    

    The PokemonListClient.tsx file is a Client Component. This component handles user interaction and local state management. It receives the initial Pokemon data and the server action from the server component. It then renders the list and handles pagination, updating the list using the provided server action.

    // inside PokemonListClient.tsx
    "use client";
    import { useState } from "react";
    
    export interface Pokemon {
      id: number;
      name: string;
    }
    
    export default function ({
      pokemon: initialPokemon,
      getPage,
    }: {
      pokemon: Pokemon[];
      getPage: (page: number) => Promise<Pokemon[]>;
    }) {
      const [pokemon, setPokemon] = useState<Pokemon[]>(initialPokemon);
      const [page, setPage] = useState(0);
    
      function setPageAndFetch(page: number) {
        setPage(page);
        getPage(page).then(setPokemon);
      }
    
      return (
        <div>
          <h1 className="text-4xl font-bold mb-5">
            Pokemon List - Page {page + 1}
          </h1>
          <div className="flex gap-6">
            <button
              onClick={async () => setPageAndFetch(page - 1)}
              disabled={page === 0}
              className="px-6 py-2 bg-blue-500 text-white rounded-full text-2xl font-bold min-w-44"
            >
              Previous
            </button>
            <button
              onClick={async () => setPageAndFetch(page + 1)}
              className="px-6 py-2 bg-blue-500 text-white rounded-full text-2xl font-bold min-w-44"
            >
              Next
            </button>
          </div>
          <ul className="text-3xl flex flex-wrap">
            {pokemon.map((pokemon) => (
              <li key={pokemon.name} className="mt-5 w-1/3">
                {pokemon.name}
              </li>
            ))}
          </ul>
        </div>
      );
    }
    

    This illustrates an emerging pattern: a symbiotic relationship between server and client components. The server component manages data fetching and server-side operations, while the client component handles interactivity and presentation.

    Naming Conventions with .server and .client

    One way developers denote this relationship is by appending .server and .client to the filenames.

    If we rename the files to PokemonList.server.tsx and PokemonList.client.tsx, their roles are clearly indicated. They can be thought of as a single logical unit for the PokemonList component.

    Updating the imports to use these new names will show everything still works.

    Directory-Based Component Model

    To further enhance organization, you can adopt a directory-based approach like we've seen previously.

    Inside of src/app create a new directory called PokemonList. Move the PokemonList.server.tsx and PokemonList.client.tsx files into this directory:

      PokemonList/
        PokemonList.server.tsx
        PokemonList.client.tsx
    

    Then create an index.ts file that exports the server component by default:

    // inside PokemonList/index.ts
    export { default } from "./PokemonList.server";
    

    Using this structure, we can import the PokemonList component like this:

    // inside app/page.tsx
    
    import PokemonList from "./PokemonList";
    
    export default function Home() {
      return <PokemonList />;
    }
    

    This directory-based approach hides the server-client component relationship, presenting a single PokemonList component to the user. It streamlines the code and prevents confusion about entry points.

    Semantic Meaning and Framework Differences

    While Next.js doesn't inherently assign special meaning to .server or .client suffixes, other frameworks like Remix do. In Remix, these suffixes explicitly define a component's execution environment.

    In Next.js, if you need a client component, you must use the 'use client' directive at the top of the file.

    Transcript