| name | Server Components Pattern |
| description | Pattern Server-First avec Next.js 16 pour data fetching optimal. MANDATORY pour toutes les pages et widgets. À utiliser lors de la création de composants, pages, ou quand l'utilisateur mentionne "fetch", "data loading", "SSR", "server component". |
| allowed-tools | Read, Write, Edit, Glob, Grep |
Server Components Pattern (Next.js 16)
🎯 Mission
Implémenter le pattern Server-First avec Next.js 16 pour un data fetching optimal, meilleur SEO, et moins de JavaScript client.
🌟 Philosophie Server-First
Depuis Next.js 16, tous les composants sont Server Components par défaut.
Pourquoi Server-First ?
- ✅ SEO optimal : Contenu pré-rendu côté serveur
- ✅ Performance : Moins de JavaScript client
- ✅ Data fetching : Accès direct à la base de données/APIs
- ✅ Security : Clés API, secrets restent côté serveur
- ✅ UX : Streaming progressif avec Suspense
📋 Decision Tree: Server vs Client Component
┌─────────────────────────────────────┐
│ Besoin d'interactivité ? │
│ (onClick, onChange, useState, etc.)│
└────────────┬────────────────────────┘
│
┌──────┴──────┐
│ │
NON OUI
│ │
▼ ▼
┌───────────┐ ┌────────────────┐
│ SERVER │ │ CLIENT │
│ COMPONENT │ │ COMPONENT │
│ │ │ "use client" │
└───────────┘ └────────────────┘
Server Components (par défaut)
Quand utiliser :
- Pages, layouts, templates
- Widgets qui affichent des données
- Composants sans interactivité
- Tout ce qui n'a PAS besoin de JavaScript client
Exemple :
// app/(dashboard)/coach/page.tsx
// Server Component (par défaut, pas de "use client")
import { getUser } from "@/lib/auth";
import { getTeams } from "@/features/teams/api/teams.server";
export default async function CoachDashboardPage() {
const user = await getUser(); // ✅ Fetch server-side
const teams = await getTeams(); // ✅ Fetch server-side
return (
<div>
<h1>Bienvenue, {user.firstName}</h1>
<TeamsList teams={teams} />
</div>
);
}
Client Components (uniquement si nécessaire)
Quand utiliser :
- Interactivité (onClick, onChange, onSubmit)
- Hooks React (useState, useEffect, useContext)
- Stores Zustand
- Browser APIs (localStorage, window, etc.)
- useOptimistic, useTransition
Exemple :
// features/teams/components/TeamForm.tsx
"use client"; // ✅ Requis pour Client Component
import { useState, useTransition } from "react";
import { createTeamAction } from "../actions/create-team.action";
export function TeamForm() {
const [name, setName] = useState("");
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
await createTeamAction({ name });
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button disabled={isPending}>
{isPending ? "Création..." : "Créer"}
</button>
</form>
);
}
🏗️ Architecture Server-First
Structure des fichiers
features/
└── teams/
├── api/
│ └── teams.server.ts # ✅ API server-side
├── actions/
│ └── create-team.action.ts # ✅ Server Actions (mutations)
├── components/
│ ├── TeamsList.tsx # ✅ Server Component (fetch)
│ ├── TeamCard.tsx # ✅ Server Component (présentation)
│ └── TeamForm.tsx # ✅ Client Component (interactivité)
└── hooks/
└── useTeamForm.ts # ✅ Custom hook (client-side logic)
app/
└── (dashboard)/
└── teams/
└── page.tsx # ✅ Server Component (async)
Template: API Server (*.server.ts)
// features/teams/api/teams.server.ts
import { serverFetch } from "@/lib/server-fetch";
import type { Team } from "@/types";
/**
* Server-side API for Teams
*
* Functions to fetch team data from Server Components
* Uses serverFetch with httpOnly cookies for auth
*/
export async function getTeams(): Promise<Team[]> {
const teams = await serverFetch<Team[]>("/teams", {
cache: "no-store", // ou "force-cache" pour caching
});
return teams || [];
}
export async function getTeam(teamId: string): Promise<Team | null> {
const team = await serverFetch<Team>(`/teams/${teamId}`, {
cache: "no-store",
});
return team;
}
Template: Server Component Page
// app/(dashboard)/teams/page.tsx
import { Suspense } from "react";
import { getTeams } from "@/features/teams/api/teams.server";
import { TeamsList } from "@/features/teams/components/TeamsList";
import { TeamsListSkeleton } from "@/features/teams/components/TeamsListSkeleton";
/**
* Teams Page - Server Component
*
* Pattern: Server Component with Suspense streaming
*/
export default async function TeamsPage() {
return (
<div className="container py-8">
<h1 className="text-3xl font-bold mb-6">Mes Équipes</h1>
<Suspense fallback={<TeamsListSkeleton />}>
<TeamsList />
</Suspense>
</div>
);
}
Template: Server Component Widget
// features/teams/components/TeamsList.tsx
import { getTeams } from "../api/teams.server";
import { TeamCard } from "./TeamCard";
/**
* TeamsList - Server Component
*
* Fetch teams server-side and display
* No client-side JavaScript for data fetching
*/
export async function TeamsList() {
const teams = await getTeams(); // ✅ Fetch server-side
if (teams.length === 0) {
return <EmptyTeamsList />;
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{teams.map(team => (
<TeamCard key={team.id} team={team} />
))}
</div>
);
}
Template: Client Component (si interactivité)
// features/teams/components/TeamCard.tsx
"use client";
import { useTransition } from "react";
import { deleteTeamAction } from "../actions/delete-team.action";
interface TeamCardProps {
team: Team;
}
export function TeamCard({ team }: TeamCardProps) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
await deleteTeamAction(team.id);
});
};
return (
<Card>
<h3>{team.name}</h3>
<Button onClick={handleDelete} disabled={isPending}>
{isPending ? "Suppression..." : "Supprimer"}
</Button>
</Card>
);
}
🔄 Pattern de Composition
Server Component parent → Client Components enfants
Règle d'or : Fetch server-side, pass props aux Client Components
// ✅ BON - Server Component parent
export async function TeamsDashboard() {
const teams = await getTeams(); // Server-side fetch
return (
<div>
<TeamsHeader /> {/* Server Component */}
<TeamsList teams={teams} /> {/* Client Component si interactivité */}
</div>
);
}
// Client Component enfant
"use client";
export function TeamsList({ teams }: { teams: Team[] }) {
const [selected, setSelected] = useState<string | null>(null);
return (
<div>
{teams.map(team => (
<div
key={team.id}
onClick={() => setSelected(team.id)}
className={selected === team.id ? "selected" : ""}
>
{team.name}
</div>
))}
</div>
);
}
❌ ANTI-PATTERN: Client Component avec useEffect pour fetch
// ❌ MAUVAIS - Client Component avec useEffect
"use client";
export function TeamsList() {
const [teams, setTeams] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadTeams() {
const data = await fetchTeams(); // ❌ Fetch client-side
setTeams(data);
setIsLoading(false);
}
void loadTeams();
}, []); // ❌ useEffect pour fetch initial = ANTI-PATTERN
if (isLoading) return <Skeleton />;
return <div>{teams.map(...)}</div>;
}
// ✅ BON - Server Component
export async function TeamsList() {
const teams = await getTeams(); // ✅ Fetch server-side
return <div>{teams.map(...)}</div>;
}
🌊 Suspense & Streaming
Pattern avec Suspense
// app/(dashboard)/teams/page.tsx
export default async function TeamsPage() {
return (
<div>
{/* Section 1 - Streams independently */}
<Suspense fallback={<TeamStatsSkeleton />}>
<TeamStats />
</Suspense>
{/* Section 2 - Streams independently */}
<Suspense fallback={<TeamsListSkeleton />}>
<TeamsList />
</Suspense>
</div>
);
}
// Chaque composant fetch ses données
async function TeamStats() {
const stats = await getTeamStats(); // Fetch 1
return <div>{stats.total} équipes</div>;
}
async function TeamsList() {
const teams = await getTeams(); // Fetch 2 (en parallèle)
return <div>{teams.map(...)}</div>;
}
Avantages :
- ✅ Parallel fetching (TeamStats et TeamsList en parallèle)
- ✅ Progressive rendering (TeamStats peut s'afficher avant TeamsList)
- ✅ Meilleure perceived performance
🔐 Auth Server-Side
Fonction getUser()
// lib/auth.ts
import { serverFetch } from "./server-fetch";
import type { User } from "@/types";
export async function getUser(): Promise<User | null> {
const user = await serverFetch<User>("/auth/profile", {
requireAuth: true,
cache: "no-store",
});
return user;
}
export async function requireAuth(): Promise<User> {
const user = await getUser();
if (!user) {
throw new Error("Unauthorized");
}
return user;
}
Usage dans une page
// app/(dashboard)/profile/page.tsx
import { requireAuth } from "@/lib/auth";
export default async function ProfilePage() {
const user = await requireAuth(); // ✅ Fetch user server-side
return (
<div>
<h1>Profil de {user.firstName}</h1>
<ProfileForm user={user} />
</div>
);
}
📊 Caching Strategy
cache: "no-store" (default)
export async function getTeams(): Promise<Team[]> {
const teams = await serverFetch<Team[]>("/teams", {
cache: "no-store", // ✅ Toujours fresh (dashboard, etc.)
});
return teams || [];
}
cache: "force-cache"
export async function getPublicStats(): Promise<Stats> {
const stats = await serverFetch<Stats>("/stats/public", {
cache: "force-cache", // ✅ Cache agressif (données statiques)
});
return stats;
}
revalidate
export async function getNews(): Promise<News[]> {
const news = await serverFetch<News[]>("/news", {
next: { revalidate: 60 }, // ✅ Revalidate toutes les 60s
});
return news || [];
}
✅ Checklist Server-First
Avant de créer un composant
- Ai-je besoin d'interactivité ? (onClick, onChange, etc.)
- Ai-je besoin de hooks React ? (useState, useEffect, etc.)
- Ai-je besoin de Browser APIs ? (localStorage, window, etc.)
Si OUI à l'une → Client Component ("use client")
Si NON à toutes → Server Component (par défaut)
Pour les pages
- Page est un Server Component (async)
- Data fetching server-side (await getTeams())
- Suspense pour le streaming
- Skeletons pour les fallbacks
- Client Components seulement pour l'interactivité
Pour les APIs
- Fichier
*.server.tspour APIs server-side - Utilise
serverFetchavec cookies - Gestion d'erreur appropriée
- Caching strategy définie
🚨 Erreurs Courantes
1. useEffect pour fetch initial
// ❌ MAUVAIS
"use client";
useEffect(() => {
fetchData().then(setData);
}, []);
// ✅ BON
export async function Component() {
const data = await getData();
return <div>{data}</div>;
}
2. "use client" partout
// ❌ MAUVAIS - Tout en Client Component
"use client";
export function Page() {
return <TeamsList />;
}
"use client";
export function TeamsList() {
const teams = useTeams(); // Custom hook qui fait fetch
return <div>{teams.map(...)}</div>;
}
// ✅ BON - Server Component + Client si besoin
export async function Page() {
const teams = await getTeams();
return <TeamsList teams={teams} />;
}
// Client seulement si interactivité
"use client";
export function TeamsList({ teams }: Props) {
const [selected, setSelected] = useState(null);
return <div>{teams.map(...)}</div>;
}
3. Props non-sérialisables
// ❌ MAUVAIS - Fonction passée de Server à Client
export async function ServerComp() {
const handleClick = () => console.log("click");
return <ClientComp onClick={handleClick} />; // ❌ ERROR
}
// ✅ BON - Server Action
export async function ServerComp() {
return <ClientComp />;
}
// Client Component définit son propre handler
"use client";
export function ClientComp() {
const handleClick = async () => {
await someAction(); // Server Action
};
return <button onClick={handleClick}>Click</button>;
}
📚 Skills Complémentaires
- suspense-streaming : Suspense et Streaming patterns
- server-actions : Server Actions pour mutations
- atomic-component : Décomposition et composition
- react-state-management : State client (Zustand)
Rappel CRITIQUE : Server Components par défaut, Client Components seulement si nécessaire. Le fetch initial des données se fait TOUJOURS côté serveur, jamais avec useEffect.