Kurs Next.js · Moduł 2: Routing i layouty
Shallow routing - nawigacja bez przeładowania
W tej lekcji9
W Metropolii Quantum 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 16 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.
Czym jest Shallow Routing?
Shallow Routing w Next.js pozwala na zmianę adresu URL bez:
- ponownego pobierania strony z serwera (Server Components i
generateMetadatanie uruchamiają się ponownie) - pełnego przeładowania strony
- utraty stanu komponentów klienckich
Komponenty, które czytają useSearchParams albo usePathname, renderują się ponownie z nowymi wartościami - i o to chodzi.
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.
Jak używać Shallow Routing w Next.js 16?
W App Router w Next.js 16 płytką nawigację realizujesz natywnymi metodami przeglądarki window.history.pushState i window.history.replaceState. Next.js (od wersji 14.1) włącza je do swojego routera, więc hooki useSearchParams i usePathname od razu widzą nowy adres, a strona nie jest ponownie pobierana z serwera. Metoda router.push z pakietu 'next/navigation' nie ma opcji shallow: przyjmuje tylko scroll i zawsze wykonuje zwykłą nawigację.
Zobaczmy, jak to wygląda w praktyce:
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
export default function QuantumFilterPanel() {
const router = useRouter();
const searchParams = useSearchParams();
// Funkcja do aktualizacji parametrów URL z zachowaniem stanu komponentu
const updateFilter = useCallback((name: string, value: string) => {
// Tworzenie nowego obiektu URLSearchParams na podstawie aktualnych parametrów
const params = new URLSearchParams(searchParams.toString());
// Aktualizacja lub dodanie nowego parametru
params.set(name, value);
// Aktualizacja URL bez pełnego przeładowania strony
window.history.pushState(null, '', '?' + params.toString()); // Kluczowa linia: płytka zmiana URL
}, [searchParams, router]);
return (
<div className="quantum-filter-panel">
<h3>Filtry Kwantowe</h3>
<div className="filter-controls">
<button onClick={() => updateFilter('category', 'transport')}>
Transport
</button>
<button onClick={() => updateFilter('category', 'accommodation')}>
Zakwaterowanie
</button>
<button onClick={() => updateFilter('sort', 'price_asc')}>
Cena: rosnąco
</button>
<button onClick={() => updateFilter('sort', 'price_desc')}>
Cena: malejąco
</button>
</div>
</div>
);
}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.
Odczytywanie parametrów w komponentach
Aby odczytać parametry zapytania w komponentach, używamy hooka useSearchParams:
'use client';
import { useSearchParams } from 'next/navigation';
export default function QuantumResults() {
const searchParams = useSearchParams();
// Odczytanie parametrów zapytania
const category = searchParams.get('category') || 'all';
const sort = searchParams.get('sort') || 'default';
return (
<div className="quantum-results">
<h2>Wyniki dla kategorii: {category}</h2>
<p>Sortowanie: {sort}</p>
{/* Tutaj renderujemy wyniki na podstawie parametrów */}
</div>
);
}Przypadki użycia Shallow Routing
1. Systemy filtrowania i sortowania
Idealnym zastosowaniem shallow routing są systemy filtrowania i sortowania, gdzie użytkownik może dynamicznie zmieniać kryteria wyświetlania danych:
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
export default function QuantumPropertyFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const applyFilters = useCallback((filters) => {
const params = new URLSearchParams(searchParams.toString());
// Aktualizacja parametrów na podstawie filtrów
Object.entries(filters).forEach(([key, value]) => {
if (value) {
params.set(key, value.toString());
} else {
params.delete(key);
}
});
// Aktualizacja URL bez przeładowania strony
window.history.pushState(null, '', '?' + params.toString());
}, [searchParams, router]);
return (
<div className="quantum-filters">
<div className="filter-section">
<h3>Filtry nieruchomości kwantowych</h3>
<div className="filter-group">
<label>Minimalna cena:</label>
<input
type="range"
min="0"
max="10000"
step="100"
onChange={(e) => applyFilters({ minPrice: e.target.value })}
/>
</div>
<div className="filter-group">
<label>Lokalizacja:</label>
<select onChange={(e) => applyFilters({ location: e.target.value })}>
<option value="">Wszystkie</option>
<option value="quantum-district">Dzielnica Kwantowa</option>
<option value="nano-heights">Nano Heights</option>
<option value="ai-valley">Dolina AI</option>
</select>
</div>
</div>
</div>
);
}2. Paginacja
Shallow routing doskonale sprawdza się w systemach paginacji, gdzie chcemy zachować bieżący stan aplikacji podczas przechodzenia między stronami:
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
export default function QuantumPagination({ totalPages }) {
const router = useRouter();
const searchParams = useSearchParams();
// Pobieranie aktualnej strony z parametrów URL lub domyślnie 1
const currentPage = parseInt(searchParams.get('page') || '1', 10);
const goToPage = useCallback((page) => {
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
// Aktualizacja URL bez przeładowania strony
window.history.pushState(null, '', '?' + params.toString());
}, [searchParams, router]);
// Generowanie przycisków paginacji
const renderPaginationButtons = () => {
const buttons = [];
for (let i = 1; i <= totalPages; i++) {
buttons.push(
<button
key={i}
onClick={() => goToPage(i)}
className={currentPage === i ? 'active' : ''}
>
{i}
</button>
);
}
return buttons;
};
return (
<div className="quantum-pagination">
<button
disabled={currentPage === 1}
onClick={() => goToPage(currentPage - 1)}
>
Poprzednia
</button>
{renderPaginationButtons()}
<button
disabled={currentPage === totalPages}
onClick={() => goToPage(currentPage + 1)}
>
Następna
</button>
</div>
);
}3. Tabele z możliwością sortowania
Kolejnym doskonałym przypadkiem użycia są tabele z możliwością sortowania kolumn:
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
export default function QuantumDataTable({ data }) {
const router = useRouter();
const searchParams = useSearchParams();
// Pobieranie aktualnej kolumny sortowania i kierunku
const sortColumn = searchParams.get('sort') || 'name';
const sortDir = searchParams.get('dir') || 'asc';
const toggleSort = useCallback((column) => {
const params = new URLSearchParams(searchParams.toString());
// Jeśli klikamy na tę samą kolumnę, zmieniamy kierunek sortowania
if (column === sortColumn) {
params.set('dir', sortDir === 'asc' ? 'desc' : 'asc');
} else {
// W przeciwnym razie, ustawiamy nową kolumnę i domyślny kierunek
params.set('sort', column);
params.set('dir', 'asc');
}
// Aktualizacja URL bez przeładowania strony
window.history.pushState(null, '', '?' + params.toString());
}, [searchParams, router, sortColumn, sortDir]);
// Funkcja do renderowania nagłówków kolumn z ikonami sortowania
const renderHeaderCell = (column, label) => {
const isSorted = sortColumn === column;
const icon = isSorted
? (sortDir === 'asc' ? '↑' : '↓')
: '↕';
return (
<th
onClick={() => toggleSort(column)}
className={isSorted ? 'sorted' : ''}
>
{label} {icon}
</th>
);
};
return (
<table className="quantum-data-table">
<thead>
<tr>
{renderHeaderCell('name', 'Nazwa')}
{renderHeaderCell('price', 'Cena')}
{renderHeaderCell('rating', 'Ocena')}
{renderHeaderCell('date', 'Data')}
</tr>
</thead>
<tbody>
{/* Tutaj renderujemy posortowane dane */}
{[...data]
.sort((a, b) => {
const aValue = a[sortColumn];
const bValue = b[sortColumn];
const modifier = sortDir === 'asc' ? 1 : -1;
return aValue < bValue ? -1 * modifier : 1 * modifier;
})
.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.price}</td>
<td>{item.rating}</td>
<td>{item.date}</td>
</tr>
))
}
</tbody>
</table>
);
}Zalety Shallow Routing
- Zachowanie stanu - Stan komponentów jest zachowany, co zapewnia płynne doświadczenie użytkownika.
- Lepsza wydajność - Brak pełnego przeładowania strony oznacza szybsze reakcje interfejsu.
- Możliwość udostępniania linków - URL jest aktualizowany, więc użytkownicy mogą udostępniać link do konkretnego widoku z określonymi filtrami.
- Historia przeglądarki - Każda zmiana jest zapisywana w historii przeglądarki, więc przycisk "wstecz" działa zgodnie z oczekiwaniami.
Ograniczenia i uwagi
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:
useEffect(() => {
// Sprawdzanie, czy parametry są prawidłowe
const page = parseInt(searchParams.get('page') || '1', 10);
if (isNaN(page) || page < 1 || page > totalPages) {
// Korekta nieprawidłowych parametrów
const params = new URLSearchParams(searchParams.toString());
params.set('page', '1');
window.history.replaceState(null, '', '?' + params.toString());
}
}, [searchParams, router, totalPages]);Porównanie z metodami w Pages Router
Jeśli wcześniej korzystałeś z Pages Router w Next.js, warto zauważyć różnicę w implementacji shallow routing:
// W Pages Router (next/router)
router.push('/search?query=quantumtech', undefined, { shallow: true });
// W App Router (od Next.js 14.1, także w Next.js 16) - natywne History API
window.history.pushState(null, '', '/search?query=quantumtech');Opcja shallow istnieje tylko w routerze z next/router. W App Routerze metoda router.push przyjmuje jedynie opcję scroll i zawsze wykonuje zwykłą nawigację, dlatego do płytkiej zmiany adresu służy window.history.pushState.
Przykład pełnej implementacji: Panel wyszukiwania nieruchomości kwantowych
Poniżej znajduje się przykład pełnej implementacji systemu wyszukiwania i filtrowania nieruchomości w Metropolii Quantum z wykorzystaniem shallow routingu:
// app/quantum-properties/page.tsx (Server Component)
import { Suspense } from 'react';
import QuantumProperties from './QuantumProperties';
export default function QuantumPropertiesPage() {
return (
<Suspense fallback={<p>Ładowanie filtrów...</p>}>
<QuantumProperties />
</Suspense>
);
}Sama logika wyszukiwania i filtrów trafia do Client Component w osobnym pliku:
// app/quantum-properties/QuantumProperties.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState, useEffect, useCallback } from 'react';
import { fetchProperties } from '@/lib/api';
// Typy dla nieruchomości kwantowych
interface QuantumProperty {
id: string;
name: string;
price: number;
location: string;
quantumLevel: number;
dimensionalRating: number;
image: string;
}
export default function QuantumProperties() {
const router = useRouter();
const searchParams = useSearchParams();
// Stan komponentu
const [properties, setProperties] = useState<QuantumProperty[]>([]);
const [loading, setLoading] = useState(true);
// Pobieranie parametrów z URL
const location = searchParams.get('location') || 'all';
const minPrice = parseInt(searchParams.get('minPrice') || '0', 10);
const maxPrice = parseInt(searchParams.get('maxPrice') || '10000', 10);
const sort = searchParams.get('sort') || 'price_asc';
const page = parseInt(searchParams.get('page') || '1', 10);
const quantumLevel = parseInt(searchParams.get('quantumLevel') || '0', 10);
// Funkcja do aktualizacji filtrów z użyciem shallow routing
const updateFilters = useCallback((newFilters: Record<string, string | number>) => {
const params = new URLSearchParams(searchParams.toString());
// Aktualizacja parametrów
Object.entries(newFilters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.set(key, value.toString());
} else {
params.delete(key);
}
});
// Zawsze wracamy do pierwszej strony przy zmianie filtrów
if (!('page' in newFilters)) {
params.set('page', '1');
}
// Aktualizacja URL bez przeładowania strony
window.history.pushState(null, '', '?' + params.toString());
}, [searchParams, router]);
// Pobieranie danych przy zmianie parametrów
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
// W rzeczywistej aplikacji, parametry byłyby przekazywane do API
const data = await fetchProperties({
location,
minPrice,
maxPrice,
quantumLevel,
sort,
page
});
setProperties(data);
} catch (error) {
console.error('Error fetching properties:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, [location, minPrice, maxPrice, sort, page, quantumLevel]);
// Sortowanie danych na podstawie parametru sort
const sortedProperties = [...properties].sort((a, b) => {
if (sort === 'price_asc') return a.price - b.price;
if (sort === 'price_desc') return b.price - a.price;
if (sort === 'quantum_level') return b.quantumLevel - a.quantumLevel;
return 0;
});
return (
<div className="quantum-properties-page">
<h1>Nieruchomości Kwantowe</h1>
{/* Panel filtrów */}
<div className="filters-panel">
<h2>Filtry</h2>
<div className="filter-group">
<label>Lokalizacja:</label>
<select
value={location}
onChange={(e) => updateFilters({ location: e.target.value })}
>
<option value="all">Wszystkie lokalizacje</option>
<option value="quantum-district">Dzielnica Kwantowa</option>
<option value="nano-heights">Nano Heights</option>
<option value="ai-valley">Dolina AI</option>
</select>
</div>
<div className="filter-group">
<label>Cena minimalna: {minPrice} QC</label>
<input
type="range"
min="0"
max="10000"
step="100"
value={minPrice}
onChange={(e) => updateFilters({ minPrice: e.target.value })}
/>
</div>
<div className="filter-group">
<label>Cena maksymalna: {maxPrice} QC</label>
<input
type="range"
min="0"
max="10000"
step="100"
value={maxPrice}
onChange={(e) => updateFilters({ maxPrice: e.target.value })}
/>
</div>
<div className="filter-group">
<label>Poziom kwantowy (min): {quantumLevel}</label>
<input
type="range"
min="0"
max="10"
value={quantumLevel}
onChange={(e) => updateFilters({ quantumLevel: e.target.value })}
/>
</div>
<div className="filter-group">
<label>Sortowanie:</label>
<select
value={sort}
onChange={(e) => updateFilters({ sort: e.target.value })}
>
<option value="price_asc">Cena: rosnąco</option>
<option value="price_desc">Cena: malejąco</option>
<option value="quantum_level">Poziom kwantowy</option>
</select>
</div>
</div>
{/* Wyniki wyszukiwania */}
<div className="search-results">
{loading ? (
<div className="loading-spinner">Ładowanie...</div>
) : (
<>
<h2>Znaleziono {sortedProperties.length} nieruchomości</h2>
<div className="properties-grid">
{sortedProperties.map(property => (
<div key={property.id} className="property-card">
<img src={property.image} alt={property.name} />
<h3>{property.name}</h3>
<p className="price">{property.price} QC</p>
<p className="location">{property.location}</p>
<p className="quantum-level">
Poziom kwantowy: {property.quantumLevel}/10
</p>
</div>
))}
</div>
{/* Paginacja */}
<div className="pagination">
<button
disabled={page === 1}
onClick={() => updateFilters({ page: page - 1 })}
>
Poprzednia
</button>
<span className="current-page">Strona {page}</span>
<button onClick={() => updateFilters({ page: page + 1 })}>
Następna
</button>
</div>
</>
)}
</div>
</div>
);
}Strona statyczna, której Client Component czyta useSearchParams, potrzebuje nad nim granicy <Suspense>: bez niej next build kończy się błędem „useSearchParams() should be wrapped in a suspense boundary”.
Podsumowanie
Shallow Routing w Next.js 16 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 Metropolii Quantum 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ępnej lekcji połączysz Parallel Routes, Intercepting Routes i Route Groups w zaawansowane układy.
Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co umożliwia shallow routing w Next.js przy zmianie adresu URL?
2. Których hooków z 'next/navigation' używasz razem z window.history.pushState przy shallow routingu z parametrami zapytania?