frontend / nextjs
Data fetching côté serveur
Explication
Ce que vous allez apprendre
- Écrire un Server Component
asyncquiawaitdirectement ses données - Repérer et corriger un waterfall de requêtes avec
Promise.all - Comprendre pourquoi un Server Component peut interroger la base de données directement
- Passer des données d'un Server Component vers un Client Component en respectant la contrainte de sérialisation
- Distinguer ce qui doit rester côté serveur de ce qui doit descendre au client
Dans quel contexte ?
La page /dashboard d'une application interne doit afficher à la fois le profil de l'utilisateur connecté et la liste de ses dix dernières commandes. Un développeur pressé écrit const user = await fetch(...) puis, juste en dessous, const orders = await fetch(...) : chaque requête attend que la précédente soit terminée, ajoutant leurs deux temps de latence bout à bout au lieu de les paralléliser. Cette leçon montre comment repérer ce genre de ralentissement et charger les données efficacement dès la conception du composant.
D'abord, comment ça marchait avant
Dans une application React classique, aller chercher des données suit toujours le même schéma. Le composant se monte, un useEffect déclenche un appel API, un état de chargement s'affiche, puis les données arrivent enfin.
Ce schéma a un coût caché. Rien n'est visible tant que le JavaScript n'a pas fini de s'exécuter dans le navigateur — l'utilisateur regarde un écran vide ou un spinner pendant ce temps.
Les Server Components de Next.js changent complètement la donne. Un composant peut être une simple fonction async qui await directement ses données AVANT même que le HTML soit envoyé au navigateur.
Résultat concret : le visiteur reçoit une page déjà remplie de contenu, sans "flash" de chargement initial.
Mais attention, une erreur très courante guette ici : le waterfall. Enchaîner les await les uns après les autres alors que les requêtes sont indépendantes fait perdre du temps pour rien, puisque chaque await ajoute sa propre latence au temps total.
La solution est simple une fois qu'on la connaît. Promise.all([...]) lance toutes les requêtes indépendantes EN MÊME TEMPS et attend qu'elles soient toutes résolues — un gain de performance immédiat, souvent négligé par les débutants.
| Approche | Requêtes indépendantes | Temps total approximatif |
|---|---|---|
await en série | 2 requêtes de 300 ms chacune | ~600 ms |
Promise.all([...]) | 2 requêtes de 300 ms chacune | ~300 ms |
Piège fréquent
Enchaîner deux await fetch(...) indépendants l'un après l'autre "parce que ça se lit bien" double le temps de réponse pour rien. Le réflexe à prendre : dès que deux requêtes ne dépendent PAS l'une de l'autre, les lancer avec Promise.all.
Une fois qu'on sait bien récupérer ses données, une autre possibilité s'ouvre : l'accès direct à la base. Puisque le code d'un Server Component tourne sur le serveur, il peut interroger directement une base de données, sans passer par une API REST intermédiaire.
Ça simplifie beaucoup d'architectures. Avant, on créait souvent un endpoint juste pour transporter une donnée du serveur vers... le serveur. Ce n'est plus nécessaire.
Il reste une règle stricte à connaître à la frontière entre serveur et client. Seules des données sérialisables en JSON peuvent passer d'un Server Component vers un Client Component en props — impossible de transmettre une fonction ou une instance de classe complexe.
Prérequis
Cette leçon suppose que tu es à l'aise avec la distinction Server/Client Component vue dans la leçon d'introduction : ici, on approfondit précisément ce qu'un Server Component peut faire.
Et la suite ? Cette leçon s'appuie directement sur la précédente : les routes et hooks de navigation permettent d'atteindre une page, celle-ci lui donne enfin du contenu réel.
Commandes & code
Data fetching côté serveur
Dans l'App Router, les Server Components peuvent être async et appeler fetch directement.
// app/products/page.tsx — Server Component async
interface Product {
id: string;
name: string;
price: number;
}
export default async function ProductsPage() {
const products: Product[] = await fetch("https://api.example.com/products").then(
(r) => r.json()
);
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — {p.price} €
</li>
))}
</ul>
);
}// Fetch parallèle : évite les cascades de requêtes (waterfall)
export default async function DashboardPage() {
// les deux appels démarrent en même temps, pas l'un après l'autre
const [user, orders] = await Promise.all([
fetch("https://api.example.com/me").then((r) => r.json()),
fetch("https://api.example.com/orders").then((r) => r.json()),
]);
return (
<div>
<UserCard user={user} />
<OrdersTable orders={orders} />
</div>
);
}// Accès direct à la base de données — pas besoin d'une API intermédiaire
// dans un Server Component, on peut importer du code serveur directement
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.query.users.findMany({
orderBy: (u, { desc }) => desc(u.createdAt),
limit: 20,
});
return <UsersList users={users} />;
}// Composition Server + Client : passer des données serveur en props
// app/products/[id]/page.tsx
import AddToCartButton from "./AddToCartButton"; // Client Component
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(`https://api.example.com/products/${id}`).then((r) =>
r.json()
);
return (
<div>
<h1>{product.name}</h1>
{/* le Server Component sérialise "product" en props vers le Client Component */}
<AddToCartButton product={product} />
</div>
);
}// components/AddToCartButton.tsx
"use client";
import { useState } from "react";
export default function AddToCartButton({ product }: { product: { id: string } }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId: product.id }),
});
setLoading(false);
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? "Ajout..." : "Ajouter au panier"}
</button>
);
}Résumé
await fetch(...)directement dans un composant serveurasync.Promise.allpour paralléliser les requêtes indépendantes et éviter les waterfalls.- Les Server Components peuvent toucher la base de données sans couche API intermédiaire.
- Seules des données sérialisables (JSON) passent des Server Components vers les Client Components.
Exercices pratiques
Mission : le dashboard qui met deux fois plus de temps que prévu
Objectif : Repérer et corriger un waterfall de requêtes, puis respecter la frontière de sérialisation entre Server et Client Component.
Contexte
app/dashboard/page.tsx fait const user = await fetch(".../me") suivi juste en dessous de const orders = await fetch(".../orders"). Chaque requête prend environ 300 ms et ne dépend pas de l'autre, mais l'équipe mesure ~600 ms de temps de réponse au lieu des ~300 ms attendus. Un développeur veut aussi passer une fonction onRefresh en props du Server Component vers un Client Component RefreshButton, ce qui provoque une erreur au build.