L'idée clé
Dans Next.js App Router, chaque composant est Server par défaut. Il s'exécute uniquement côté serveur, pendant le rendu, et envoie du HTML pur au navigateur. Tu n'as accès à useState, useEffect, onClick, ni à window / localStorage.
Pour faire de l'interactivité (un bouton qui fait quelque chose, un formulaire contrôlé, un compteur), tu dois marquer le fichier "use client" en première ligne — il devient un Client Component.
// Server Component (par défaut) — peut faire du SQL, du fs, des secrets
import { prisma } from "@/lib/prisma";
export default async function ProductList() {
const products = await prisma.product.findMany();
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}// Client Component — interactif, mais pas d'accès direct à la DB
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}Ce que chaque type peut / ne peut pas
| Capacité | Server | Client |
|---|---|---|
async / await Prisma, fetch | ✅ | ❌ direct (fetch via API au lieu) |
useState, useEffect, useRef | ❌ | ✅ |
onClick, onChange, etc. | ❌ | ✅ |
Lire process.env.SECRET | ✅ | ❌ (exposé au public !) |
Importer node:fs | ✅ | ❌ |
| Taille du bundle JS envoyé | 0 ko | tout le code est shippé au browser |
La règle d'or
Mets le "use client" le plus bas possible dans l'arbre. Un layout server qui inclut un composant client : seul le client est shippé en JS. Un layout client qui inclut un server : tout est forcé en client.
Pratique : isole l'interactivité dans des composants dédiés (souvent suffixés -client.tsx chez wari) et garde le reste en server. Tu réduis le JS envoyé → ton site charge plus vite.
Le cas typique : page server + composant client
// app/admin/produits/page.tsx (Server Component)
import { prisma } from "@/lib/prisma";
import { ProductForm } from "./product-form-client";
export default async function ProduitsPage() {
const products = await prisma.product.findMany(); // SQL direct, OK
return (
<div>
<h1>{products.length} produits</h1>
<ProductForm initial={products} /> {/* Client Component */}
</div>
);
}// app/admin/produits/product-form-client.tsx
"use client";
import { useState } from "react";
export function ProductForm({ initial }) {
const [name, setName] = useState("");
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}Le page.tsx reste server (fait le SQL), et délègue juste la partie interactive au composant client.
Piège classique
Tu ne peux pas passer un callback (fonction) d'un Server à un Client component, ni un composant React dans un import (sauf via children). Les seules choses sérialisables qui traversent : string, number, Date, Array, Object plats, et children (slot).
Si tu écris <ClientComp onSubmit={() => ...} /> depuis un Server, ça crashe au build : "Functions cannot be passed directly to Client Components."