Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Shallow Routing - Nawigacja Bez Przeładowania

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.

Czym jest Shallow Routing?

Shallow Routing w Next.js pozwala na zmianę adresu URL bez:

  • Uruchamiania funkcji danych (getData, generateMetadata itp.)
  • Ponownego renderowania komponentów
  • Utraty stanu komponentów klienckich

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 15?

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.

Odczytywanie parametrów w komponentach

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}

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:

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}

2. Paginacja

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}

3. Tabele z możliwością sortowania

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}

Zalety Shallow Routing

  1. Zachowanie stanu - Stan komponentów jest zachowany, co zapewnia płynne doświadczenie użytkownika.
  2. Lepsza wydajność - Brak pełnego przeładowania strony oznacza szybsze reakcje interfejsu.
  3. Możliwość udostępniania linków - URL jest aktualizowany, więc użytkownicy mogą udostępniać link do konkretnego widoku z określonymi filtrami.
  4. Historia przeglądarki - Każda zmiana jest zapisywana w historii przeglądarki, więc przycisk "wstecz" działa zgodnie z oczekiwaniami.

Ograniczenia i uwagi

  1. 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.

  2. 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.

  3. 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.

  4. 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]);

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:

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 });

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 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}

Podsumowanie

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.

Przejdź do CodeWorlds