Agent skill
Server Components Pattern
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".
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/design/server-components-pattern-romualdp-hoki
SKILL.md
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.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?