03 oct. 2025·6 min de lecture

Next.js App Router : mélanges serveur et client — corrections

Apprenez à repérer les mélanges serveur/client dans Next.js App Router qui provoquent des plantages runtime, et comment restructurer composants, actions et récupération de données.

Next.js App Router : mélanges serveur et client — corrections

Qu'est-ce qu'un mélange serveur-uniquement vs client-uniquement ?

Un mélange serveur-uniquement vs client-uniquement arrive quand du code s'exécute au mauvais endroit.

Dans Next.js App Router, certains composants sont pensés pour s'exécuter sur le serveur (sûrs pour les secrets, appels directs à la base de données, APIs privées). D'autres sont faits pour s'exécuter dans le navigateur (clics de boutons, état local, accès à window). Quand ces responsabilités s'emmêlent, vous verrez des plantages, des problèmes d'hydratation, ou des builds qui échouent seulement après le déploiement.

Un modèle mental utile :

  • Les Server Components récupèrent et préparent les données, puis passent des props simples.
  • Les Client Components gèrent l'interaction, mais ne doivent pas inclure du code réservé au serveur.

Souvent, tout semble correct en développement parce que le mode dev est plus tolérant. Le rechargement à chaud, le bundling différent et le timing peuvent masquer les problèmes de frontière. Les builds de production sont plus stricts sur ce qui peut être envoyé au navigateur, et l'hydratation est moins tolérante aux désaccords.

Signes courants :

  • Page blanche après navigation (erreur visible seulement dans la console)
  • Erreurs d'hydratation où l'UI clignote puis casse
  • Erreurs 500 inattendues lors du rendu
  • “window is not defined” ou “document is not defined”
  • Erreurs de build indiquant l'importation de modules serveur dans des fichiers clients

Le code généré par IA rend cela plus fréquent parce qu'il copie des extraits sans respecter les frontières. Un exemple typique : ajouter "use client" à une page entière pour corriger une erreur de hook, alors que la page importe un helper de base de données ou lit des secrets.

Comment App Router sépare les Server et Client Components

Dans l'App Router, chaque composant est par défaut un Server Component. Cette seule règle explique la plupart des surprises.

Server Components (par défaut)

Les Server Components s'exécutent sur le serveur. Utilisez-les pour la récupération de données, la lecture de cookies/headers, l'utilisation de secrets d'environnement, et tout travail lourd que vous ne voulez pas dans le navigateur.

Si ça touche votre base de données, des clefs privées d'API, ou une session d'auth, gardez-le côté serveur et passez les résultats en props.

Client Components (optionnel)

Un composant devient Client Component seulement lorsque vous ajoutez "use client" en haut du fichier. Les Client Components s'exécutent dans le navigateur, donc ils peuvent utiliser l'état, les effets, les gestionnaires d'événements et des APIs navigateur comme localStorage.

La frontière fonctionne ainsi :

  • Un Server Component peut importer un Client Component. Tout ce qui est sous ce Client Component s'exécutera côté client.
  • Un Client Component ne peut pas importer un Server Component ni des modules réservés au serveur.

Vous avez généralement besoin de "use client" quand un composant utilise des hooks comme useState/useEffect, des événements navigateur comme onClick, ou des APIs navigateur comme window et document.

Vous n'avez pas besoin de "use client" pour de l'UI pure qui affiche juste des props. Une correction courante (surtout dans les prototypes générés par IA) est de garder la page et le chargement des données côté serveur, puis de rendre un petit Client Component uniquement pour la partie interactive (un filtre, une modale, ou un éditeur inline).

Schémas de plantage runtime que vous pouvez reconnaître rapidement

La plupart des plantages App Router se réduisent à un problème : du code réservé au navigateur s'exécute sur le serveur, ou du code serveur se retrouve dans le bundle du navigateur.

Schéma 1 : Hooks dans un Server Component

Si vous voyez des erreurs du type “React Hook ... is not supported in Server Components” ou “You're importing a component that needs useState/useEffect,” vérifiez l'en-tête du fichier. Si le fichier ne commence pas par "use client", React le traite comme un Server Component.

Schéma 2 : Modules serveur importés dans du code client

Les erreurs mentionnant fs, path, crypto, ou “Module not found: Can't resolve 'fs'” signifient souvent qu'un composant client importe un helper partagé qui (peut-être indirectement) importe du code réservé à Node.

Cela arrive fréquemment quand un fichier utils ou lib partagé mélange des helpers serveur et clients, et que le client l'importe “juste pour une fonction”.

Schéma 3 : APIs navigateur utilisées pendant le rendu serveur

“window is not defined”, “document is not defined”, et “localStorage is not defined” signifient que le code s'exécute côté serveur. Ça peut être un Server Component, une Server Action, ou même un module importé pendant le rendu serveur.

Schéma 4 : Appeler une logique serveur depuis le client sans un pont sûr

Ces problèmes apparaissent sous forme de “You're importing a Server Action into a Client Component,” “Server-only module cannot be imported from a Client Component,” ou un appel côté client qui déclenche par erreur une fonction qui n'était pas destinée au navigateur.

Étape par étape : trouver la mauvaise frontière dans votre arbre de composants

Les gains les plus rapides viennent de rendre le plantage reproductible. Testez-le en dev puis en build de production. “Ça marche en dev” n'excuse pas les problèmes de frontière.

Quand vous lisez l'erreur, arrêtez-vous au premier fichier que vous possédez réellement. Les frames de framework sont bruyantes. Le premier fichier dans votre repo est généralement l'endroit où l'import ou le type de composant erroné entre dans l'arbre.

Un flux de travail simple :

  • Reproduisez le crash de manière fiable (même route, même action, même état utilisateur).
  • Dans la stack trace, sautez au premier fichier app et notez quel composant l'a rendu.
  • Vérifiez l'en-tête du fichier : est-ce un Server Component par défaut, ou commence-t-il par "use client" ?
  • Suivez les imports jusqu'à trouver le premier désaccord :
    • import serveur utilisé par du code client (fs, clients DB, next/headers)
    • utilisation navigateur dans du code serveur (window, document, localStorage)
  • Décidez de la propriété : secrets et données côté serveur, état UI et événements côté client.

Une erreur très fréquente : une page serveur passe un client de base de données, des données dérivées de cookies, ou un helper serveur dans un composant client. Ça casse. Récupérez côté serveur, puis passez des données JSON simples.

Restructurer les composants qui mélangent préoccupations serveur et client

Les plantages arrivent généralement quand un composant essaie de tout faire.

Une séparation fiable :

  • Server Component : récupère les données, vérifie l'auth, utilise des secrets.
  • Client Component : gère l'état, les événements, les effets, et toute UI dépendante du DOM.

Remontez le travail de données dans l'arbre. Récupérez côté serveur (ou via une fonction serveur appelée par lui), puis passez le résultat comme props simples. Ça empêche le code serveur d'aller dans le bundle du navigateur.

Ensuite, isolez l'interactivité. Gardez les parties client petites pour ne pas envoyer toute la page au navigateur juste pour rendre un bouton interactif.

À la frontière serveur→client, gardez les props « ennuyeuses » : chaînes, nombres, booléens, tableaux, objets simples. Ne passez pas de clients de DB, d'objets request, d'instances de classes ou de fonctions.

Exemple : une page de tableau de bord récupère info utilisateur, abonnement, et activités récentes, et a aussi des filtres, une modale et un graphique. Récupérez tout dans DashboardPage (serveur) et passez { userName, plan, activityItems } à un composant DashboardControls client qui gère l'état des filtres et l'ouverture de la modale.

Server Actions : schémas sûrs pour formulaires et mutations

Find the real boundary bug
We’ll pinpoint the first bad server-client boundary and the imports causing your crash.

Les Server Actions fonctionnent bien quand un utilisateur envoie un formulaire et vous devez modifier des données : créer un enregistrement, mettre à jour un profil, réinitialiser un mot de passe, ou exécuter un petit workflow.

Une structure sûre est de garder l'UI du formulaire dans un Client Component, et la mutation dans un fichier server-only exporté comme action. Le client gère les entrées, l'état de chargement, et l'affichage des erreurs. Le serveur gère les vérifications d'auth, la validation et les appels DB.

// actions.ts
'use server'

export async function updateProfile(formData: FormData) {
  const name = String(formData.get('name') ?? '')
  // validate, check auth, write to DB
  return { ok: true }
}

Côté client, ne passez que ce dont le serveur a besoin. Ne passez pas de secrets, tokens, ou objets utilisateur bruts via des props juste pour faire marcher une action. Si l'action a besoin de savoir qui est l'utilisateur, lisez-le côté serveur (cookies/session) à l'intérieur de l'action.

Deux habitudes évitent la plupart des fuites :

  • Valider les entrées et revérifier l'autorisation dans l'action.
  • Retourner des erreurs sûres et conviviales, pas des traces de stack.

Si vous voulez une UI optimiste, gardez-la locale et limitée. Évitez de transformer toute une page en Client Component juste pour afficher un spinner.

Récupération de données dans App Router sans travail en double

Beaucoup de problèmes de frontière commencent par une double récupération de données.

Dans l'App Router, la valeur par défaut devrait être la récupération côté serveur près de la route. Vous aurez un premier rendu plus rapide, des bundles navigateur plus petits, et vous garderez les secrets hors du client.

Récupérez côté client seulement quand c'est réellement nécessaire (polling, widgets temps réel, ou un bouton de rafraîchissement qui met à jour une section précise).

Un bug courant : le rendu serveur récupère des données, puis un Client Component monte et récupère les mêmes données avec useEffect. Ça peut causer du flicker, des problèmes de rate-limit et des désaccords confus.

Un flux propre ressemble à ceci :

  • La requête atteint une route
  • Le fetch serveur obtient les données (DB, API interne, ou tiers)
  • Les Server Components rendent la page avec ces données
  • Les Client Components gèrent les interactions et déclenchent des mises à jour ciblées

Le cache peut aussi masquer des problèmes pendant les tests. Si les données semblent aléatoirement périmées, vérifiez si votre fetch est mis en cache et si la revalidation est configurée comme vous l'attendez.

Auth et secrets : ce qui doit rester sur le serveur

Get a practical repair plan
Get a clear plan to stabilize the codebase before adding more features.

Les problèmes d'auth démarrent souvent comme une erreur de frontière : un Client Component touche quelque chose qui ne devrait jamais quitter le serveur. Parfois vous avez des plantages ou des redirections étranges. Parfois vous fuyez silencieusement des secrets dans le bundle client.

Les fuites les plus courantes dans le code généré :

  • Lire des variables d'environnement dans un Client Component
  • Mettre une config dans un fichier partagé importé à la fois par serveur et client

Si voir ça dans DevTools serait douloureux, ça ne devrait pas être accessible depuis le code client.

Gardez les vérifications d'auth et la logique de rôles sur le serveur. Le client peut afficher des états UI, mais il ne doit pas être la source de vérité pour « cet utilisateur est-il autorisé ? »

Évitez de stocker des tokens sensibles dans localStorage par défaut. C'est facile à inspecter et peut être volé via XSS.

Brièvetés à surveiller :

  • Boucles de redirection quand serveur et client essaient tous deux de protéger la même route
  • Mismatches de session où le serveur rend un état et le client hydrate dans un autre
  • Confusion runtime Edge vs Node pour les bibliothèques d'auth
  • “Ça marche localement, casse en prod” quand les vars d'environnement diffèrent et que les bundles clients changent

Erreurs courantes qui ramènent les plantages

La plupart des plantages récurrents ne sont pas des bugs mystérieux du framework. Ce sont les mêmes erreurs de frontière, patchées à la va-vite, puis réintroduites.

Quelques motifs reviennent souvent :

  • Ajouter "use client" à une grosse page pour faire taire une erreur de hook
  • Garder des helpers “partagés” qui mélangent code serveur-only et code sûr pour le client
  • Créer ou importer un client de base de données dans des fichiers de composants (ça se propage vite via les imports)
  • Appeler fetch() vers votre propre route API depuis un Server Component par habitude, alors que vous pourriez appeler le code serveur directement
  • Corriger par essais/erreurs au lieu de suivre le premier import fautif dans la stack trace

Un exemple typique : une page dashboard plante seulement en production parce qu'elle importe getUser() (lit des cookies, serveur-only), mais la page était marquée "use client" pour supporter un graphique. La correction durable est de déplacer le graphique dans son propre Client Component et de garder la page server-first.

Checklist rapide avant de déployer

La plupart des plantages App Router arrivent parce qu'un fichier fait deux jobs.

Vérification de frontière

Demandez-vous pour chaque composant : ce fichier peut-il s'exécuter dans le navigateur ?

Si oui, il ne doit pas toucher aux secrets, variables d'environnement serveur-only, clients DB, ou bibliothèques Node-only. Si vous voyez ces imports, déplacez le travail dans un Server Component, une Server Action, ou une route serveur.

Vérification finale :

  • Les APIs navigateur (window, document, localStorage, navigator) et les hooks signifient Client Component. Gardez la logique serveur en dehors.
  • Les secrets et imports serveur-only signifient Server Component. Passez seulement les données dont l'UI a besoin.
  • Les props qui traversent la frontière doivent être sérialisables (objets simples, tableaux, chaînes, nombres). Évitez les instances de classe, BigInt, et les fonctions.
  • Pour les écritures (formulaires, updates, deletes), utilisez une Server Action ou une route serveur.
  • Testez un build de production localement, pas seulement next dev.

Une habitude pratique

Avant de déployer, parcourez vos principaux flux après un build propre. Si une page plante seulement en mode production, c'est généralement un problème de frontière, une prop non sérialisable, ou un import serveur-only qui fuit dans le client.

Exemple : corriger une page dashboard qui plante

End the trial-and-error fixes
We trace the stack to the first file you own and fix the source, not symptoms.

Une situation classique : une page dashboard a besoin de données récupérées côté serveur et d'interactions (filtres de date, toggles, recherche).

Ce qui a mal tourné

La première version mélange souvent les responsabilités dans un seul fichier. Par exemple, app/dashboard/page.tsx récupère des données côté serveur, mais utilise aussi useState, lit localStorage, ou appelle window.matchMedia pour mémoriser des réglages. Ça marche dans le navigateur, mais la page est par défaut un Server Component, donc ça peut planter avec “window is not defined” ou “Hooks can only be used in a Client Component.”

Autre glissement fréquent : l'UI du filtre est marquée 'use client', mais elle importe un helper serveur qui lit des cookies ou interroge une DB privée. Ça peut déclencher des erreurs du type “You’re importing a Server Component into a Client Component.”

Une restructuration simple qui arrête les plantages

Faites que la page possède les données, et que le composant client possède l'interactivité.

Sur le serveur (page) : récupérez les données et rendez-les.

// app/dashboard/page.tsx (Server Component)
import Filters from './Filters';
import { getDashboardData } from './data';

export default async function Page() {
  const data = await getDashboardData();
  return (
    <>
      <Filters initial={data.filters} />
      {/* render table using data.items */}
    </>
  );
}

Côté client (filters) : gardez l'état et les événements UI locaux, et envoyez les changements via une Server Action.

// app/dashboard/actions.ts
'use server';
export async function updateFilters(next) {
  // validate input, save, return safe data
  return { ok: true };
}

Résultat : moins de plantages runtime, propriété claire (les fetchs et secrets restent côté serveur, le client gère les clics), et les mises à jour passent par un seul chemin sûr.

Étapes suivantes si votre code App Router continue de casser

Si vous vous battez avec le même plantage encore et encore, c'est généralement un problème de frontière à l'échelle du projet : du code client importe des modules serveur-only, du code serveur importe des hooks, ou les mutations sont dispersées côté client.

Les bases de code générées par IA venant d'outils comme Lovable, Bolt, v0, Cursor, ou Replit répètent souvent ces erreurs parce qu'elles mélangent des patterns qui marchaient avec des anciennes configurations mais qui ne tiennent pas face à la stricte séparation de l'App Router.

Quand les mêmes symptômes apparaissent sur plusieurs pages, une refactorisation ciblée est souvent plus rapide que des patchs.

Si vous avez hérité d'un prototype généré par IA et voulez un diagnostic rapide et structuré, FixMyMess (fixmymess.ai) se spécialise dans la réparation et le durcissement de ce type de code Next.js, en commençant par un audit gratuit pour identifier les premiers problèmes de frontière et les imports risqués.