Frontend·3 min de lecture

Server Components vs Client Components

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éServerClient
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 kotout 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."

Dans ton code wari