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 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.