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

    The release of the App Router was a sea change in terms of how we develop our next JS applications. And as we've seen through the months, we've actually started to see some interesting new patterns evolve in how we structure our applications, including how we name our components. Take for example, this application where we've got a list of Pokemon. So this is a single component called PokemonList. You can page through it backwards and forwards.

    It calls the Pokemon API. And let's go take a look at the code. So in the instructions you'll have a link to the client server component code. We look down in the source app you've got two different components here. You've got Pokemon list that is a React server component part of this duo that creates the Pokemon list.

    You've got the React server component that primarily goes off and makes the request for the initial Pokemon list from the API, and then has a server action that you can use to get another page of data as you page through it. And then it brings in the client component, which is the other part of that duo. And all it really does is just invoke the client component with the initial set of data and then that server action. And then the client component takes that initial set of Pokemon and that server action. It initializes its local state that has the current Pokemon list with the initial Pokemon and sets the page to zero.

    And then we have the set page and fetch function that's called by the next page and previous page buttons. It sets the page and then it calls that server action that then gets back the data for the Pokémon and sets the Pokémon. It's very simple, but it is demonstrative of how we have this emerging pattern of having React Server components coupled to React Client components in this symbiotic relationship. You've got the React Server components that handle everything on the server in terms of getting the data and then making subsequent requests and the client component that adds the interactivity. So one thing in this space that folks have been doing is they've been using .server and .client to show this relationship.

    So let me show you that to you. So our Pokemon list, which is the RSC, will become PokemonList.Server.TSX. Just make sure that got changed over there. Yes, that's fine. Then the PokemonListClient would become PokemonList.client.tsx.

    Let's make sure that worked. Yep, that made change. Cool. So having that .client and that server connotes that strong coupling, and that those two components together are really one component called Pokemon list. To take that even one step further, we can use a directory-based component model like we've seen previously.

    Create a new folder called Pokemon list. We drag those two files into it. Then our index in this case, exports the default from the server. Now, if I look back over at my page, all I need to do is just bring in PokemonList and it looks like a simple component. Now it's hidden from our consuming code that we have this symbiotic relationship between these two components.

    All we know is we have a Pokemon list and it just happens to work even though some of the functionality is on the server and other portions are on the client. Now this is a really good model for how to organize your files when it comes to components that really shouldn't be used separately. These are meant to be used in conjunction with each other. And really, it shouldn't be up to the client in this case, the user of those components, to know that I need to use server as the entry point. This directory-based approach, where we're exporting it from index, hides the fact that the server is the main entry point.

    Now I will say that Next.js doesn't apply any semantic meaning to the .client or .server that you put onto the file name here. But other frameworks do. For example, Remix actually has .client and .server as part of their system. And if you have a .client file, that is the component for the client. If you have a .server file, that is the component for the server.

    So there is some semantic meaning on the Remix side. That is not the case on the Next.js side. In the Next.js side, if you want to have a client component, you have to specify the useClient directive.