W QuantumCity, inżynierowie transportu opracowali specjalną technologię umożliwiającą modyfikację trasy pojazdu bez konieczności jego zatrzymywania i ponownego uruchamiania. Ta innowacja, zwana "Płytką Zmianą Trajektorii", pozwala na dynamiczną zmianę parametrów podróży bez przerywania doświadczenia pasażera - nie ma zatrzymań, nie ma opóźnień, wszystko dzieje się płynnie podczas ruchu.
W Next.js 15, istnieje podobna koncepcja zwana "Shallow Routing" (płytką nawigacją). To potężna technika, która pozwala na aktualizację adresu URL strony bez pełnego przeładowania strony, zachowując przy tym stan komponentów. Jest to szczególnie przydatne w aplikacjach, które wymagają dynamicznej zmiany parametrów zapytania (query parameters) bez tracenia stanu komponentów.
Shallow Routing w Next.js pozwala na zmianę adresu URL bez:
Jest to idealne rozwiązanie dla filtrów, sortowania, paginacji i innych scenariuszy, gdzie chcemy zachować kontekst użytkownika, jednocześnie aktualizując URL dla potrzeb historii przeglądarki lub możliwości udostępniania.
W App Router w Next.js 15, implementacja shallow routing opiera się na używaniu hooka
useRouter z pakietu 'next/navigation' wraz z parametrem shallow: true podczas wywoływania metody push.Zobaczmy, jak to wygląda w praktyce:
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 // Funkcja do aktualizacji parametrów URL z zachowaniem stanu komponentu
11 const updateFilter = useCallback((name: string, value: string) => {
12 // Tworzenie nowego obiektu URLSearchParams na podstawie aktualnych parametrów
13 const params = new URLSearchParams(searchParams.toString());
14
15 // Aktualizacja lub dodanie nowego parametru
16 params.set(name, value);
17
18 // Aktualizacja URL bez pełnego przeładowania strony
19 router.push('?' + params.toString(), {
20 shallow: true, // Kluczowy parametr dla shallow routing
21 });
22 }, [searchParams, router]);
23
24 return (
25 <div className="quantum-filter-panel">
26 <h3>Filtry Kwantowe</h3>
27 <div className="filter-controls">
28 <button onClick={() => updateFilter('category', 'transport')}>
29 Transport
30 </button>
31 <button onClick={() => updateFilter('category', 'accommodation')}>
32 Zakwaterowanie
33 </button>
34 <button onClick={() => updateFilter('sort', 'price_asc')}>
35 Cena: rosnąco
36 </button>
37 <button onClick={() => updateFilter('sort', 'price_desc')}>
38 Cena: malejąco
39 </button>
40 </div>
41 </div>
42 );
43}W powyższym przykładzie, kliknięcie na przyciski filtrów aktualizuje URL, dodając parametry zapytania, ale nie powoduje pełnego przeładowania strony. Dzięki temu zachowujemy stan aplikacji, np. dane formularza wprowadzone przez użytkownika, pozycję przewijania, czy stan lokalny komponentów.
Aby odczytać parametry zapytania w komponentach, używamy hooka
useSearchParams:1'use client';
2
3import { useSearchParams } from 'next/navigation';
4
5export default function QuantumResults() {
6 const searchParams = useSearchParams();
7
8 // Odczytanie parametrów zapytania
9 const category = searchParams.get('category') || 'all';
10 const sort = searchParams.get('sort') || 'default';
11
12 return (
13 <div className="quantum-results">
14 <h2>Wyniki dla kategorii: {category}</h2>
15 <p>Sortowanie: {sort}</p>
16
17 {/* Tutaj renderujemy wyniki na podstawie parametrów */}
18 </div>
19 );
20}Idealnym zastosowaniem shallow routing są systemy filtrowania i sortowania, gdzie użytkownik może dynamicznie zmieniać kryteria wyświetlania danych:
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 // Aktualizacja parametrów na podstawie filtrów
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 // Aktualizacja URL bez przeładowania strony
23 router.push('?' + params.toString(), { shallow: true });
24 }, [searchParams, router]);
25
26 return (
27 <div className="quantum-filters">
28 <div className="filter-section">
29 <h3>Filtry nieruchomości kwantowych</h3>
30 <div className="filter-group">
31 <label>Minimalna cena:</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>Lokalizacja:</label>
42 <select onChange={(e) => applyFilters({ location: e.target.value })}>
43 <option value="">Wszystkie</option>
44 <option value="quantum-district">Dzielnica Kwantowa</option>
45 <option value="nano-heights">Nano Heights</option>
46 <option value="ai-valley">Dolina AI</option>
47 </select>
48 </div>
49 </div>
50 </div>
51 );
52}Shallow routing doskonale sprawdza się w systemach paginacji, gdzie chcemy zachować bieżący stan aplikacji podczas przechodzenia między stronami:
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 // Pobieranie aktualnej strony z parametrów URL lub domyślnie 1
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 // Aktualizacja URL bez przeładowania strony
18 router.push('?' + params.toString(), { shallow: true });
19 }, [searchParams, router]);
20
21 // Generowanie przycisków paginacji
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 Poprzednia
45 </button>
46
47 {renderPaginationButtons()}
48
49 <button
50 disabled={currentPage === totalPages}
51 onClick={() => goToPage(currentPage + 1)}
52 >
53 Następna
54 </button>
55 </div>
56 );
57}Kolejnym doskonałym przypadkiem użycia są tabele z możliwością sortowania kolumn:
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 // Pobieranie aktualnej kolumny sortowania i kierunku
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 // Jeśli klikamy na tę samą kolumnę, zmieniamy kierunek sortowania
18 if (column === sortColumn) {
19 params.set('dir', sortDir === 'asc' ? 'desc' : 'asc');
20 } else {
21 // W przeciwnym razie, ustawiamy nową kolumnę i domyślny kierunek
22 params.set('sort', column);
23 params.set('dir', 'asc');
24 }
25
26 // Aktualizacja URL bez przeładowania strony
27 router.push('?' + params.toString(), { shallow: true });
28 }, [searchParams, router, sortColumn, sortDir]);
29
30 // Funkcja do renderowania nagłówków kolumn z ikonami sortowania
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', 'Nazwa')}
52 {renderHeaderCell('price', 'Cena')}
53 {renderHeaderCell('rating', 'Ocena')}
54 {renderHeaderCell('date', 'Data')}
55 </tr>
56 </thead>
57 <tbody>
58 {/* Tutaj renderujemy posortowane dane */}
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}Kompatybilność z SEO - Jeśli filtry i sortowanie są istotne dla SEO, może być konieczne użycie statycznego generowania stron (SSG) dla kluczowych kombinacji parametrów.
Serwer vs. Klient - Shallow routing działa tylko po stronie klienta, więc początkowe renderowanie strony zawsze będzie oparte na parametrach otrzymanych z serwera.
Kolejność wykonania kodu - Należy pamiętać, że podczas shallow routing nie są wykonywane funkcje danych, więc logika oparta na tych funkcjach nie zostanie uruchomiona.
Obsługa błędów - Dobrą praktyką jest dodanie obsługi błędów, aby uniknąć problemów z nieprawidłowymi parametrami URL:
1useEffect(() => {
2 // Sprawdzanie, czy parametry są prawidłowe
3 const page = parseInt(searchParams.get('page') || '1', 10);
4
5 if (isNaN(page) || page < 1 || page > totalPages) {
6 // Korekta nieprawidłowych parametrów
7 const params = new URLSearchParams(searchParams.toString());
8 params.set('page', '1');
9 router.push('?' + params.toString(), { shallow: true });
10 }
11}, [searchParams, router, totalPages]);Jeśli wcześniej korzystałeś z Pages Router w Next.js, warto zauważyć różnicę w implementacji shallow routing:
1// W Pages Router (starszy sposób)
2router.push('/search?query=quantumtech', undefined, { shallow: true });
3
4// W App Router (Next.js 13+)
5router.push('/search?query=quantumtech', { shallow: true });Poniżej znajduje się przykład pełnej implementacji systemu wyszukiwania i filtrowania nieruchomości w QuantumCity z wykorzystaniem shallow routing:
1// app/quantum-properties/page.tsx
2'use client';
3
4import { useRouter, useSearchParams } from 'next/navigation';
5import { useState, useEffect, useCallback } from 'react';
6import { fetchProperties } from '@/lib/api';
7
8// Typy dla nieruchomości kwantowych
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 QuantumPropertiesPage() {
20 const router = useRouter();
21 const searchParams = useSearchParams();
22
23 // Stan komponentu
24 const [properties, setProperties] = useState<QuantumProperty[]>([]);
25 const [loading, setLoading] = useState(true);
26
27 // Pobieranie parametrów z 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 // Funkcja do aktualizacji filtrów z użyciem shallow routing
36 const updateFilters = useCallback((newFilters: Record<string, string | number>) => {
37 const params = new URLSearchParams(searchParams.toString());
38
39 // Aktualizacja parametrów
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 // Zawsze wracamy do pierwszej strony przy zmianie filtrów
49 if (!('page' in newFilters)) {
50 params.set('page', '1');
51 }
52
53 // Aktualizacja URL bez przeładowania strony
54 router.push('?' + params.toString(), { shallow: true });
55 }, [searchParams, router]);
56
57 // Pobieranie danych przy zmianie parametrów
58 useEffect(() => {
59 const fetchData = async () => {
60 setLoading(true);
61 try {
62 // W rzeczywistej aplikacji, parametry byłyby przekazywane do 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 // Sortowanie danych na podstawie parametru sort
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>Nieruchomości Kwantowe</h1>
93
94 {/* Panel filtrów */}
95 <div className="filters-panel">
96 <h2>Filtry</h2>
97
98 <div className="filter-group">
99 <label>Lokalizacja:</label>
100 <select
101 value={location}
102 onChange={(e) => updateFilters({ location: e.target.value })}
103 >
104 <option value="all">Wszystkie lokalizacje</option>
105 <option value="quantum-district">Dzielnica Kwantowa</option>
106 <option value="nano-heights">Nano Heights</option>
107 <option value="ai-valley">Dolina AI</option>
108 </select>
109 </div>
110
111 <div className="filter-group">
112 <label>Cena minimalna: {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>Cena maksymalna: {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>Poziom kwantowy (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>Sortowanie:</label>
148 <select
149 value={sort}
150 onChange={(e) => updateFilters({ sort: e.target.value })}
151 >
152 <option value="price_asc">Cena: rosnąco</option>
153 <option value="price_desc">Cena: malejąco</option>
154 <option value="quantum_level">Poziom kwantowy</option>
155 </select>
156 </div>
157 </div>
158
159 {/* Wyniki wyszukiwania */}
160 <div className="search-results">
161 {loading ? (
162 <div className="loading-spinner">Ładowanie...</div>
163 ) : (
164 <>
165 <h2>Znaleziono {sortedProperties.length} nieruchomości</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 Poziom kwantowy: {property.quantumLevel}/10
176 </p>
177 </div>
178 ))}
179 </div>
180
181 {/* Paginacja */}
182 <div className="pagination">
183 <button
184 disabled={page === 1}
185 onClick={() => updateFilters({ page: page - 1 })}
186 >
187 Poprzednia
188 </button>
189
190 <span className="current-page">Strona {page}</span>
191
192 <button onClick={() => updateFilters({ page: page + 1 })}>
193 Następna
194 </button>
195 </div>
196 </>
197 )}
198 </div>
199 </div>
200 );
201}Shallow Routing w Next.js 15 to potężna technika, która pozwala na tworzenie interaktywnych i dynamicznych interfejsów użytkownika z zachowaniem stanu aplikacji podczas aktualizacji URL. Jest to idealne rozwiązanie dla systemów filtrowania, sortowania, paginacji i innych przypadków użycia, gdzie chcemy płynnie aktualizować zawartość strony bez pełnego przeładowania.
Podobnie jak "Płytka Zmiana Trajektorii" w QuantumCity pozwala pojazdom na dynamiczną zmianę trasy bez zatrzymywania, tak Shallow Routing w Next.js pozwala na dynamiczną aktualizację parametrów URL bez przerywania doświadczenia użytkownika.
W następnym rozdziale, omówimy obsługę stron 404 i techniki przekierowań w Next.js 15, które są niezbędne do tworzenia kompletnych i odpornych na błędy aplikacji.