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é :
"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
fetchs’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 :
// 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 :
"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 navigateurrouter.push()modifie l’URL sans déclencher de requête côté clientgetFirstQ()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=phonerenvoie 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
useStateet 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
searchParamsavant de rendre - Les entrées client se synchronisent avec
useSearchParams() - Gérer plusieurs paramètres avec
getAll()
— Editorial Team
Aucun commentaire pour le moment.