Retour à l'accueil

URL comme source de l'état Next.js App Router

Dans Next.js App Router, l'URL devient la source unique de l'état de la page. Les composants serveur lisent searchParams et forment le contenu avant le rendu. Le formulaire client se synchronise avec l'historique du navigateur sans useEffect supplémentaire.

L'URL contrôle les données dans Next.js App Router
Advertisement 728x90

URL comme source unique de vérité dans Next.js App Router

Dans le routeur d'application Next.js, l'URL devient la référence incontestable de l'état de la page. Contrairement à la synchronisation locale de useState avec la barre d'adresse, la page rendue côté serveur lit directement searchParams et génère immédiatement le contenu. Cette approche élimine les désynchronisations entre les champs de saisie, les données et la navigation.

Pour une recherche de produits, cela fonctionne ainsi : l'URL contient ?q=phone → le serveur extrait le paramètre → effectue un appel API → affiche une liste prête à être visualisée. La navigation arrière/avant et les liens directs se comportent de manière prévisible.

Pièges courants des approches côté client

Le code React traditionnel utilisant useEffect crée plusieurs sources de vérité :

Google AdInline article slot
"use client";

import { useEffect, useState } from "react";

export default function GoodsPage() {
  const [q, setQ] = useState("");
  const [items, setItems] = useState([]);

  useEffect(() => {
    async function load() {
      const res = await fetch(`/api/goods?q=${encodeURIComponent(q)}`);
      const data = await res.json();
      setItems(data.products);
    }

    load();
  }, [q]);

  return (
    <>
      <input value={q} onChange={e => setQ(e.target.value)} />
      <div>{items.map(item => <div key={item.id}>{item.title}</div>)}</div>
    </>
  );
}

Conséquences :

  • Les liens directs ne restituent pas l’état
  • Le rechargement de page réinitialise les données
  • La navigation arrière/avant perturbe l’expérience utilisateur
  • Une synchronisation manuelle entre champ et URL est nécessaire

Modèle côté serveur : données avant rendu

Les composants serveur lisent searchParams côté serveur et récupèrent les données avant l’hydratation :

  • L’URL stocke les paramètres de filtrage
  • Le serveur analyse searchParams
  • Une requête fetch s’exécute avec mise en cache
  • L’interface s’affiche à partir de props pré-chargés

Une couche de données universelle pour les produits :

Google AdInline article slot
// src/app/_data/dummyjson.js
const API_BASE = "https://dummyjson.com";

async function fetchJson(url, fetchOptions = {}) {
  const res = await fetch(url, fetchOptions);

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    const err = new Error(`Erreur DummyJSON : ${res.status} ${res.statusText}. ${text}`);
    err.status = res.status;
    throw err;
  }

  return res.json();
}

export async function getProducts({ q = "", limit = 12, skip = 0 } = {}) {
  const safeQ = String(q).trim();
  const qs = new URLSearchParams({
    limit: String(limit),
    skip: String(skip),
  });

  const url = safeQ
    ? `${API_BASE}/products/search?${qs.toString()}&q=${encodeURIComponent(safeQ)}`
    : `${API_BASE}/products?${qs.toString()}`;

  return fetchJson(url, {
    next: { revalidate: 60 },
  });
}

La logique de recherche est isolée — les composants page restent propres et centrés sur leur rôle.

Implémentation de la page côté serveur

// src/app/(app)/goods/page.js
import Link from "next/link";
import { getProducts } from "@/app/_data/goodsApi";
import GoodsSearchBar from "@/app/_ui/GoodsSearchBar";
import GoodsGridMotionClient from "@/app/_ui/GoodsGridMotionClient";

export default async function GoodsPage({ searchParams }) {
  const sp = await searchParams;
  const q = typeof sp?.q === "string" ? sp.q : "";

  const data = await getProducts({ q, limit: 12, skip: 0 });

  const isEmpty = !data?.products?.length;
  const hasFilter = !!String(q || "").trim();

  return (
    <div>
      <h1>Produits</h1>
      <GoodsSearchBar initialQuery={q} />

      {isEmpty ? (
        <div>
          <h2>Aucun résultat trouvé</h2>

          {hasFilter ? (
            <p>Il n’y a aucun produit correspondant à "{q}"</p>
          ) : (
            <p>La liste est vide</p>
          )}

          <Link href="/goods">Voir tous les produits</Link>
        </div>
      ) : (
        <GoodsGridMotionClient products={data.products} />
      )}
    </div>
  );
}

L’URL détermine entièrement le contenu de la page.

Recherche côté client avec synchronisation URL

L’entrée reste côté client, mais met à jour l’URL uniquement après soumission :

Google AdInline article slot
"use client";

import { useEffect, useMemo, useState } from "react";

import { useRouter, useSearchParams } from "next/navigation";

function getFirstQ(sp) {
  const all = sp.getAll("q");
  return all[0] ?? "";
}

export default function GoodsSearchBar({ basePath = "/goods" }) {
  const router = useRouter();
  const sp = useSearchParams();

  const qFromUrl = getFirstQ(sp);
  const [value, setValue] = useState(qFromUrl);

  useEffect(() => {
    setValue(qFromUrl);
  }, [qFromUrl]);

  function buildHref(nextQ) {
    const q = nextQ.trim();
    return q ? `${basePath}?q=${encodeURIComponent(q)}` : basePath;
  }

  function onSubmit(e) {
    e.preventDefault();
    router.push(buildHref(value));
  }

  function onReset() {
    router.push(basePath);
  }

  const canSearch = useMemo(() => value.trim().length > 0, [value]);

  return (
    <form onSubmit={onSubmit}>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="ex. téléphone"
      />

      <button type="submit" disabled={!canSearch}>
        Rechercher
      </button>

      <button type="button" onClick={onReset}>
        Réinitialiser
      </button>
    </form>
  );
}

Points clés :

  • useSearchParams() maintient l’entrée synchronisée avec l’historique du navigateur
  • router.push() modifie l’URL sans déclencher de requête côté client
  • getFirstQ() gère les paramètres de requête multiples

Avantages de cette architecture

  • Source unique de vérité : l’URL définit l’état de la page
  • Liens directs : /goods?q=phone renvoie toujours les mêmes résultats
  • Navigation : retour/arrière restaure exactement l’état
  • Rendu côté serveur : les données chargent avant l’hydratation
  • Débogage : l’état est visible dans l’URL

Quand éviter les paramètres d’URL

Bonnes candidats pour les paramètres d’URL :

  • Requêtes de recherche
  • Tri et filtres
  • Pagination
  • Modes d’affichage

Mieux conservés dans l’état client :

  • Listes déroulantes et modales
  • Animations
  • Entrées temporaires
  • États au survol ou au focus

Trois erreurs critiques à éviter

  • État double sans synchronisation : mélanger useState et URL sans coordination
  • Requête côté client dans des composants serveur via useEffect
  • Entrée comme source principale plutôt que l’URL après soumission

Ce qui compte le plus :

  • L’URL doit définir les données de la page dans App Router
  • Les composants serveur lisent searchParams avant de rendre
  • Les entrées client se synchronisent avec useSearchParams()
  • Gérer plusieurs paramètres avec getAll()

— Editorial Team

Advertisement 728x90

Lire ensuite