Cours Next.js · Module 2: Routing et layouts
Shallow Routing - navigation sans rechargement de page
Dans cette leçon9
À Metropolis Quantum, les ingénieurs des transports ont développé une technologie spéciale qui permet de modifier l'itinéraire d'un véhicule sans avoir à l'arrêter ni à le redémarrer. Cette innovation, appelée « Shallow Trajectory Change », permet de modifier dynamiquement les paramètres du voyage sans interrompre l'expérience du passager - pas d'arrêts, pas de délais, tout se passe en douceur en mouvement.
Dans Next.js 16, il existe un concept similaire appelé « Shallow Routing ». C'est une technique puissante qui permet de mettre à jour l'URL de la page sans rechargement complet, tout en préservant l'état des composants. Elle est particulièrement utile dans les applications qui nécessitent de modifier dynamiquement les paramètres de requête sans perdre l'état des composants.
Qu'est-ce que le Shallow Routing ?
Le shallow routing dans Next.js permet de changer l'URL sans :
- récupérer à nouveau la page depuis le serveur (les Server Components et
generateMetadatane s'exécutent pas à nouveau) - recharger complètement la page
- perdre l'état des composants client
Les composants qui lisent useSearchParams ou usePathname sont re-rendus avec les nouvelles valeurs - c'est tout l'intérêt.
C'est une solution idéale pour les filtres, le tri, la pagination et d'autres scénarios où l'on veut préserver le contexte de l'utilisateur tout en mettant à jour l'URL à des fins d'historique du navigateur ou de partage.
Comment utiliser le Shallow Routing dans Next.js 16 ?
Dans l'App Router de Next.js 16, tu réalises le shallow routing avec les méthodes natives du navigateur window.history.pushState et window.history.replaceState. Next.js (depuis la version 14.1) les intègre à son routeur : les hooks useSearchParams et usePathname voient tout de suite la nouvelle adresse, et la page n'est pas redemandée au serveur. La méthode router.push du paquet 'next/navigation' n'a pas d'option shallow : elle n'accepte que scroll et effectue toujours une navigation classique.
Voyons comment cela se présente en pratique :
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumFilterPanel() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Fonction pour mettre à jour les paramètres de l'URL tout en préservant l'état du composant
11 const updateFilter = useCallback((name: string, value: string) => {
12 // Création d'un nouvel objet URLSearchParams à partir des paramètres actuels
13 const params = new URLSearchParams(searchParams.toString());
14
15 // Met à jour ou ajoute un nouveau paramètre
16 params.set(name, value);
17
18 // Met à jour l'URL sans rechargement complet de la page
19 window.history.pushState(null, '', '?' + params.toString()); // Ligne clé : un changement d'URL superficiel
20 }, [searchParams, router]);
21
22 return (
23 <div className="quantum-filter-panel">
24 <h3>Filtres Quantum</h3>
25 <div className="filter-controls">
26 <button onClick={() => updateFilter('category', 'transport')}>
27 Transport
28 </button>
29 <button onClick={() => updateFilter('category', 'accommodation')}>
30 Hébergement
31 </button>
32 <button onClick={() => updateFilter('sort', 'price_asc')}>
33 Prix : croissant
34 </button>
35 <button onClick={() => updateFilter('sort', 'price_desc')}>
36 Prix : décroissant
37 </button>
38 </div>
39 </div>
40 );
41}Dans l'exemple ci-dessus, un clic sur les boutons de filtre met à jour l'URL en ajoutant des paramètres de requête, mais ne provoque pas de rechargement complet de la page. On préserve ainsi l'état de l'application, comme les données de formulaire saisies par l'utilisateur, la position de défilement ou l'état local du composant.
Lire les paramètres dans les composants
Pour lire les paramètres de requête dans les composants, on utilise le hook useSearchParams :
1'use client';
2
3import { useSearchParams } from 'next/navigation';
4
5export default function QuantumResults() {
6 const searchParams = useSearchParams();
7
8 // Lit les paramètres de requête
9 const category = searchParams.get('category') || 'all';
10 const sort = searchParams.get('sort') || 'default';
11
12 return (
13 <div className="quantum-results">
14 <h2>Résultats pour la catégorie : {category}</h2>
15 <p>Tri : {sort}</p>
16
17 {/* Ici, on rend les résultats à partir des paramètres */}
18 </div>
19 );
20}Cas d'usage du Shallow Routing
1. Systèmes de filtrage et de tri
Un cas d'usage idéal du shallow routing est constitué des systèmes de filtrage et de tri, où l'utilisateur peut modifier dynamiquement les critères d'affichage des données :
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumPropertyFilters() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 const applyFilters = useCallback((filters) => {
11 const params = new URLSearchParams(searchParams.toString());
12
13 // Met à jour les paramètres en fonction des filtres
14 Object.entries(filters).forEach(([key, value]) => {
15 if (value) {
16 params.set(key, value.toString());
17 } else {
18 params.delete(key);
19 }
20 });
21
22 // Met à jour l'URL sans rechargement de page
23 window.history.pushState(null, '', '?' + params.toString());
24 }, [searchParams, router]);
25
26 return (
27 <div className="quantum-filters">
28 <div className="filter-section">
29 <h3>Filtres de propriétés Quantum</h3>
30 <div className="filter-group">
31 <label>Prix minimum :</label>
32 <input
33 type="range"
34 min="0"
35 max="10000"
36 step="100"
37 onChange={(e) => applyFilters({ minPrice: e.target.value })}
38 />
39 </div>
40 <div className="filter-group">
41 <label>Emplacement :</label>
42 <select onChange={(e) => applyFilters({ location: e.target.value })}>
43 <option value="">Tous</option>
44 <option value="quantum-district">Quantum District</option>
45 <option value="nano-heights">Nano Heights</option>
46 <option value="ai-valley">AI Valley</option>
47 </select>
48 </div>
49 </div>
50 </div>
51 );
52}2. Pagination
Le shallow routing fonctionne à merveille dans les systèmes de pagination, où l'on veut préserver l'état actuel de l'application tout en naviguant entre les pages :
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumPagination({ totalPages }) {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Récupère la page actuelle depuis les paramètres de l'URL, ou 1 par défaut
11 const currentPage = parseInt(searchParams.get('page') || '1', 10);
12
13 const goToPage = useCallback((page) => {
14 const params = new URLSearchParams(searchParams.toString());
15 params.set('page', page.toString());
16
17 // Met à jour l'URL sans rechargement de page
18 window.history.pushState(null, '', '?' + params.toString());
19 }, [searchParams, router]);
20
21 // Génère les boutons de pagination
22 const renderPaginationButtons = () => {
23 const buttons = [];
24 for (let i = 1; i <= totalPages; i++) {
25 buttons.push(
26 <button
27 key={i}
28 onClick={() => goToPage(i)}
29 className={currentPage === i ? 'active' : ''}
30 >
31 {i}
32 </button>
33 );
34 }
35 return buttons;
36 };
37
38 return (
39 <div className="quantum-pagination">
40 <button
41 disabled={currentPage === 1}
42 onClick={() => goToPage(currentPage - 1)}
43 >
44 Précédent
45 </button>
46
47 {renderPaginationButtons()}
48
49 <button
50 disabled={currentPage === totalPages}
51 onClick={() => goToPage(currentPage + 1)}
52 >
53 Suivant
54 </button>
55 </div>
56 );
57}3. Tableaux triables
Un autre excellent cas d'usage est constitué des tableaux à colonnes triables :
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumDataTable({ data }) {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Récupère la colonne et la direction de tri actuelles
11 const sortColumn = searchParams.get('sort') || 'name';
12 const sortDir = searchParams.get('dir') || 'asc';
13
14 const toggleSort = useCallback((column) => {
15 const params = new URLSearchParams(searchParams.toString());
16
17 // Si on clique sur la même colonne, on inverse la direction du tri
18 if (column === sortColumn) {
19 params.set('dir', sortDir === 'asc' ? 'desc' : 'asc');
20 } else {
21 // Sinon, on définit une nouvelle colonne et la direction par défaut
22 params.set('sort', column);
23 params.set('dir', 'asc');
24 }
25
26 // Met à jour l'URL sans rechargement de page
27 window.history.pushState(null, '', '?' + params.toString());
28 }, [searchParams, router, sortColumn, sortDir]);
29
30 // Fonction pour rendre les en-têtes de colonne avec les icônes de tri
31 const renderHeaderCell = (column, label) => {
32 const isSorted = sortColumn === column;
33 const icon = isSorted
34 ? (sortDir === 'asc' ? '↑' : '↓')
35 : '↕';
36
37 return (
38 <th
39 onClick={() => toggleSort(column)}
40 className={isSorted ? 'sorted' : ''}
41 >
42 {label} {icon}
43 </th>
44 );
45 };
46
47 return (
48 <table className="quantum-data-table">
49 <thead>
50 <tr>
51 {renderHeaderCell('name', 'Nom')}
52 {renderHeaderCell('price', 'Prix')}
53 {renderHeaderCell('rating', 'Note')}
54 {renderHeaderCell('date', 'Date')}
55 </tr>
56 </thead>
57 <tbody>
58 {/* Ici, on rend les données triées */}
59 {[...data]
60 .sort((a, b) => {
61 const aValue = a[sortColumn];
62 const bValue = b[sortColumn];
63 const modifier = sortDir === 'asc' ? 1 : -1;
64
65 return aValue < bValue ? -1 * modifier : 1 * modifier;
66 })
67 .map(item => (
68 <tr key={item.id}>
69 <td>{item.name}</td>
70 <td>{item.price}</td>
71 <td>{item.rating}</td>
72 <td>{item.date}</td>
73 </tr>
74 ))
75 }
76 </tbody>
77 </table>
78 );
79}Avantages du Shallow Routing
- Préservation de l'état - L'état des composants est préservé, garantissant une expérience utilisateur fluide.
- Meilleures performances - L'absence de rechargement complet de la page signifie des réponses d'interface plus rapides.
- Possibilité de partager des liens - L'URL est mise à jour, ce qui permet aux utilisateurs de partager un lien vers une vue précise avec des filtres précis.
- Historique du navigateur - Chaque changement est enregistré dans l'historique du navigateur, donc le bouton « retour » fonctionne comme prévu.
Limites et remarques
Compatibilité SEO - Si les filtres et le tri sont importants pour le SEO, il peut être nécessaire d'utiliser la génération de site statique (SSG) pour les combinaisons de paramètres clés.
Serveur vs client - Le shallow routing ne fonctionne que côté client, donc le rendu initial de la page sera toujours basé sur les paramètres reçus du serveur.
Ordre d'exécution du code - N'oublie pas que pendant le shallow routing, les fonctions de données ne sont pas exécutées, donc la logique basée sur ces fonctions ne sera pas déclenchée.
Gestion des erreurs - C'est une bonne pratique d'ajouter une gestion des erreurs pour éviter les problèmes liés à des paramètres d'URL invalides :
1useEffect(() => {
2 // Vérifie si les paramètres sont valides
3 const page = parseInt(searchParams.get('page') || '1', 10);
4
5 if (isNaN(page) || page < 1 || page > totalPages) {
6 // Corrige les paramètres invalides
7 const params = new URLSearchParams(searchParams.toString());
8 params.set('page', '1');
9 window.history.replaceState(null, '', '?' + params.toString());
10 }
11}, [searchParams, router, totalPages]);Comparaison avec les méthodes du Pages Router
Si tu as utilisé auparavant le Pages Router dans Next.js, il vaut la peine de noter la différence d'implémentation du shallow routing :
1// Dans le Pages Router (next/router)
2router.push('/search?query=quantumtech', undefined, { shallow: true });
3
4// Dans l'App Router (depuis Next.js 14.1, aussi dans Next.js 16) - l'API History native
5window.history.pushState(null, '', '/search?query=quantumtech');L'option shallow n'existe que dans le routeur de next/router. Dans l'App Router, router.push n'accepte que l'option scroll et effectue toujours une navigation classique : c'est pourquoi on utilise window.history.pushState pour un changement d'adresse superficiel.
Exemple d'implémentation complète : panneau de recherche de propriétés Quantum
Voici un exemple d'implémentation complète d'un système de recherche et de filtrage de propriétés à Metropolis Quantum, utilisant le shallow routing :
1// app/quantum-properties/page.tsx (Server Component)
2import { Suspense } from 'react';
3import QuantumProperties from './QuantumProperties';
4
5export default function QuantumPropertiesPage() {
6 return (
7 <Suspense fallback={<p>Chargement des filtres...</p>}>
8 <QuantumProperties />
9 </Suspense>
10 );
11}La logique de recherche et de filtrage elle-même va dans un Client Component, dans un fichier séparé :
1// app/quantum-properties/QuantumProperties.tsx
2'use client';
3
4import { useRouter, useSearchParams } from 'next/navigation';
5import { useState, useEffect, useCallback } from 'react';
6import { fetchProperties } from '@/lib/api';
7
8// Types pour les propriétés Quantum
9interface QuantumProperty {
10 id: string;
11 name: string;
12 price: number;
13 location: string;
14 quantumLevel: number;
15 dimensionalRating: number;
16 image: string;
17}
18
19export default function QuantumProperties() {
20 const router = useRouter();
21 const searchParams = useSearchParams();
22
23 // État du composant
24 const [properties, setProperties] = useState<QuantumProperty[]>([]);
25 const [loading, setLoading] = useState(true);
26
27 // Récupère les paramètres depuis l'URL
28 const location = searchParams.get('location') || 'all';
29 const minPrice = parseInt(searchParams.get('minPrice') || '0', 10);
30 const maxPrice = parseInt(searchParams.get('maxPrice') || '10000', 10);
31 const sort = searchParams.get('sort') || 'price_asc';
32 const page = parseInt(searchParams.get('page') || '1', 10);
33 const quantumLevel = parseInt(searchParams.get('quantumLevel') || '0', 10);
34
35 // Fonction pour mettre à jour les filtres via le shallow routing
36 const updateFilters = useCallback((newFilters: Record<string, string | number>) => {
37 const params = new URLSearchParams(searchParams.toString());
38
39 // Met à jour les paramètres
40 Object.entries(newFilters).forEach(([key, value]) => {
41 if (value !== undefined && value !== null && value !== '') {
42 params.set(key, value.toString());
43 } else {
44 params.delete(key);
45 }
46 });
47
48 // Toujours revenir à la première page lors d'un changement de filtres
49 if (!('page' in newFilters)) {
50 params.set('page', '1');
51 }
52
53 // Met à jour l'URL sans rechargement de page
54 window.history.pushState(null, '', '?' + params.toString());
55 }, [searchParams, router]);
56
57 // Récupère les données quand les paramètres changent
58 useEffect(() => {
59 const fetchData = async () => {
60 setLoading(true);
61 try {
62 // Dans une vraie application, les paramètres seraient passés à l'API
63 const data = await fetchProperties({
64 location,
65 minPrice,
66 maxPrice,
67 quantumLevel,
68 sort,
69 page
70 });
71 setProperties(data);
72 } catch (error) {
73 console.error('Error fetching properties:', error);
74 } finally {
75 setLoading(false);
76 }
77 };
78
79 fetchData();
80 }, [location, minPrice, maxPrice, sort, page, quantumLevel]);
81
82 // Trie les données en fonction du paramètre de tri
83 const sortedProperties = [...properties].sort((a, b) => {
84 if (sort === 'price_asc') return a.price - b.price;
85 if (sort === 'price_desc') return b.price - a.price;
86 if (sort === 'quantum_level') return b.quantumLevel - a.quantumLevel;
87 return 0;
88 });
89
90 return (
91 <div className="quantum-properties-page">
92 <h1>Propriétés Quantum</h1>
93
94 {/* Panneau de filtres */}
95 <div className="filters-panel">
96 <h2>Filtres</h2>
97
98 <div className="filter-group">
99 <label>Emplacement :</label>
100 <select
101 value={location}
102 onChange={(e) => updateFilters({ location: e.target.value })}
103 >
104 <option value="all">Tous les emplacements</option>
105 <option value="quantum-district">Quantum District</option>
106 <option value="nano-heights">Nano Heights</option>
107 <option value="ai-valley">AI Valley</option>
108 </select>
109 </div>
110
111 <div className="filter-group">
112 <label>Prix minimum : {minPrice} QC</label>
113 <input
114 type="range"
115 min="0"
116 max="10000"
117 step="100"
118 value={minPrice}
119 onChange={(e) => updateFilters({ minPrice: e.target.value })}
120 />
121 </div>
122
123 <div className="filter-group">
124 <label>Prix maximum : {maxPrice} QC</label>
125 <input
126 type="range"
127 min="0"
128 max="10000"
129 step="100"
130 value={maxPrice}
131 onChange={(e) => updateFilters({ maxPrice: e.target.value })}
132 />
133 </div>
134
135 <div className="filter-group">
136 <label>Niveau quantique (min) : {quantumLevel}</label>
137 <input
138 type="range"
139 min="0"
140 max="10"
141 value={quantumLevel}
142 onChange={(e) => updateFilters({ quantumLevel: e.target.value })}
143 />
144 </div>
145
146 <div className="filter-group">
147 <label>Tri :</label>
148 <select
149 value={sort}
150 onChange={(e) => updateFilters({ sort: e.target.value })}
151 >
152 <option value="price_asc">Prix : croissant</option>
153 <option value="price_desc">Prix : décroissant</option>
154 <option value="quantum_level">Niveau quantique</option>
155 </select>
156 </div>
157 </div>
158
159 {/* Résultats de recherche */}
160 <div className="search-results">
161 {loading ? (
162 <div className="loading-spinner">Chargement...</div>
163 ) : (
164 <>
165 <h2>{sortedProperties.length} propriétés trouvées</h2>
166
167 <div className="properties-grid">
168 {sortedProperties.map(property => (
169 <div key={property.id} className="property-card">
170 <img src={property.image} alt={property.name} />
171 <h3>{property.name}</h3>
172 <p className="price">{property.price} QC</p>
173 <p className="location">{property.location}</p>
174 <p className="quantum-level">
175 Niveau quantique : {property.quantumLevel}/10
176 </p>
177 </div>
178 ))}
179 </div>
180
181 {/* Pagination */}
182 <div className="pagination">
183 <button
184 disabled={page === 1}
185 onClick={() => updateFilters({ page: page - 1 })}
186 >
187 Précédent
188 </button>
189
190 <span className="current-page">Page {page}</span>
191
192 <button onClick={() => updateFilters({ page: page + 1 })}>
193 Suivant
194 </button>
195 </div>
196 </>
197 )}
198 </div>
199 </div>
200 );
201}Une page statique dont le Client Component lit useSearchParams a besoin d'une frontière <Suspense> au-dessus : sans elle, next build échoue avec « useSearchParams() should be wrapped in a suspense boundary ».
Résumé
Le Shallow Routing dans Next.js 16 est une technique puissante qui permet de créer des interfaces utilisateur interactives et dynamiques tout en préservant l'état de l'application lors des mises à jour de l'URL. C'est une solution idéale pour le filtrage, le tri, la pagination et d'autres cas d'usage où l'on veut mettre à jour en douceur le contenu de la page sans rechargement complet.
Tout comme le « Shallow Trajectory Change » à Metropolis Quantum permet aux véhicules de changer dynamiquement d'itinéraire sans s'arrêter, le Shallow Routing dans Next.js permet de mettre à jour dynamiquement les paramètres de l'URL sans interrompre l'expérience de l'utilisateur.
Dans la prochaine leçon, tu combineras les Parallel Routes, les Intercepting Routes et les Route Groups dans des mises en page avancées.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que permet le shallow routing dans Next.js lors du changement d’URL ?
2. Quels hooks de 'next/navigation' utilises-tu avec window.history.pushState pour le shallow routing avec des paramètres de requête ?