W międzygalaktycznych podróżach, ważne jest śledzenie trasy i możliwość powrotu do poprzednich punktów eksploracji. W świecie React Router, tym systemem nawigacji jest historia przeglądarki - mechanizm, który pozwala na poruszanie się wstecz i wprzód między odwiedzonymi stronami. Zobaczmy, jak świadomie zarządzać historią, aby zwiększyć komfort podróży użytkowników przez naszą aplikację.
Historia przeglądarki to stos odwiedzonych przez użytkownika adresów URL. Przeglądarki zapewniają API, które pozwala na:
React Router integruje się z tym API, dostarczając hooki i komponenty do zarządzania historią w sposób przyjazny dla Reacta.
Hook
useNavigate jest podstawowym narzędziem do programowej nawigacji w React Router:1import { useNavigate } from 'react-router-dom';
2
3function MissionControl() {
4 const navigate = useNavigate();
5
6 const goToExploration = () => {
7 navigate('/exploration');
8 };
9
10 const goBack = () => {
11 navigate(-1); // Cofnij o jedną stronę
12 };
13
14 const goForward = () => {
15 navigate(1); // Przejdź o jedną stronę do przodu
16 };
17
18 const jumpToSpecificPoint = () => {
19 navigate(-3); // Cofnij o trzy strony
20 };
21
22 return (
23 <div className="mission-control">
24 <h1>Centrum Sterowania Misją</h1>
25
26 <div className="navigation-panel">
27 <button onClick={goToExploration}>Rozpocznij eksplorację</button>
28 <button onClick={goBack}>Powrót do poprzedniej lokalizacji</button>
29 <button onClick={goForward}>Do przodu</button>
30 <button onClick={jumpToSpecificPoint}>Powrót do punktu wyjścia</button>
31 </div>
32 </div>
33 );
34}Przy użyciu
navigate możemy zdecydować, czy chcemy dodać nową stronę do historii, czy zastąpić bieżącą:1// Dodanie nowej strony do stosu historii (domyślne zachowanie)
2navigate('/new-location');
3
4// Zastąpienie bieżącej strony w historii
5navigate('/new-location', { replace: true });Kiedy warto używać
replace: true?Czasami potrzebujemy zapobiec opuszczeniu strony przez użytkownika, na przykład gdy ma niezapisane zmiany w formularzu. W React Router v6 nie ma wbudowanego mechanizmu blokowania nawigacji, ale możemy go zaimplementować:
1import { useCallback, useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3
4function useBlockNavigation(blocker, when = true) {
5 const navigate = useNavigate();
6 const location = useLocation();
7
8 // Funkcja przedstawiająca użytkownikowi monit
9 const handleBlockNavigation = useCallback((nextLocation) => {
10 if (
11 when &&
12 nextLocation.location.pathname !== location.pathname &&
13 !window.confirm('Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?')
14 ) {
15 // Użytkownik kliknął "Anuluj", zapobiegamy nawigacji
16 return false;
17 }
18 // Użytkownik kliknął "OK", pozwalamy na nawigację
19 return true;
20 }, [when, location]);
21
22 // Ustawiamy nasłuchiwanie na zdarzenie beforeunload
23 useEffect(() => {
24 if (when) {
25 const handleBeforeUnload = (e) => {
26 e.preventDefault();
27 e.returnValue = '';
28 };
29
30 window.addEventListener('beforeunload', handleBeforeUnload);
31
32 return () => {
33 window.removeEventListener('beforeunload', handleBeforeUnload);
34 };
35 }
36 }, [when]);
37
38 // Ustawiamy własne blokowanie dla React Router
39 useEffect(() => {
40 if (when) {
41 // Tutaj byśmy użyli mechanizmu blocker z React Router,
42 // ale w v6 musimy użyć workaround - np. window.history.pushState
43 const unblock = () => {
44 // Funkcja czyszcząca
45 };
46
47 return unblock;
48 }
49 }, [when, handleBlockNavigation, navigate]);
50}
51
52// Użycie:
53function SpaceshipConfigurator() {
54 const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
55 const [config, setConfig] = useState({});
56
57 useBlockNavigation(hasUnsavedChanges);
58
59 // Reszta komponentu...
60}W nowszych wersjach React Router można użyć oficjalnego podejścia z pakietem
@remix-run/router.Hook
useLocation daje nam dostęp do bieżącego obiektu location, który zawiera informacje o aktualnym URL:1import { useLocation } from 'react-router-dom';
2
3function LocationTracker() {
4 const location = useLocation();
5
6 return (
7 <div>
8 <h2>Aktualna Lokalizacja Kosmiczna</h2>
9 <p>Ścieżka: {location.pathname}</p>
10 <p>Query: {location.search}</p>
11 <p>Hash: {location.hash}</p>
12 <p>Kluczowy identyfikator: {location.key}</p>
13 <p>Metoda nawigacji: {location.state?.navigationMethod || 'Standardowa'}</p>
14 </div>
15 );
16}Pole
location.key jest szczególnie przydatne - jest to unikalny identyfikator dla każdego wpisu w historii. Możemy go wykorzystać do wykrywania, kiedy użytkownik powraca do strony, którą już odwiedził.Możemy stworzyć własny hook do śledzenia historii nawigacji:
1import { useState, useEffect } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useNavigationHistory(maxEntries = 10) {
5 const location = useLocation();
6 const [history, setHistory] = useState([]);
7
8 useEffect(() => {
9 setHistory(prev => {
10 // Dodanie bieżącej lokalizacji do historii
11 const newHistory = [...prev, {
12 pathname: location.pathname,
13 search: location.search,
14 hash: location.hash,
15 key: location.key,
16 timestamp: Date.now()
17 }];
18
19 // Ograniczenie rozmiaru historii
20 return newHistory.slice(-maxEntries);
21 });
22 }, [location, maxEntries]);
23
24 return history;
25}
26
27// Użycie:
28function NavigationLogger() {
29 const history = useNavigationHistory(5);
30
31 return (
32 <div className="navigation-log">
33 <h3>Dziennik Podróży</h3>
34 <ul>
35 {history.map((entry, index) => (
36 <li key={entry.key}>
37 {index === history.length - 1 ? '➡️ ' : ''}
38 {entry.pathname} (odwiedzono: {new Date(entry.timestamp).toLocaleTimeString()})
39 </li>
40 ))}
41 </ul>
42 </div>
43 );
44}Czasami chcemy zmienić domyślne zachowanie przycisku "Wstecz" w przeglądarce. Na przykład, jeśli użytkownik jest na stronie, którą może odwiedzić tylko po zalogowaniu, a kliknie "Wstecz", możemy go przekierować do bezpiecznej lokalizacji:
1import { useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3
4function SecureZone() {
5 const navigate = useNavigate();
6 const location = useLocation();
7 const { isAuthenticated } = useAuth();
8
9 useEffect(() => {
10 // Sprawdzamy, czy użytkownik jest zalogowany przy każdej zmianie lokalizacji
11 if (!isAuthenticated) {
12 navigate('/login', { replace: true });
13 }
14
15 // Zmodyfikowane zachowanie przycisku "Wstecz"
16 const handlePopState = () => {
17 if (!isAuthenticated) {
18 // Jeśli użytkownik nie jest zalogowany i kliknie "Wstecz",
19 // przekieruj go do strony głównej zamiast poprzedniej strony
20 navigate('/', { replace: true });
21 }
22 };
23
24 window.addEventListener('popstate', handlePopState);
25
26 return () => {
27 window.removeEventListener('popstate', handlePopState);
28 };
29 }, [isAuthenticated, navigate, location]);
30
31 return (
32 <div className="secure-zone">
33 <h1>Strefa Zabezpieczona</h1>
34 <p>Witaj w Centrum Dowodzenia. Masz dostęp do tajnych danych.</p>
35 </div>
36 );
37}W bardziej złożonych aplikacjach, możemy chcieć synchronizować historię przeglądarki z globalnym stanem aplikacji (np. Redux):
1import { useEffect } from 'react';
2import { useLocation } from 'react-router-dom';
3import { useDispatch } from 'react-redux';
4
5function HistorySync() {
6 const location = useLocation();
7 const dispatch = useDispatch();
8
9 useEffect(() => {
10 // Za każdym razem, gdy zmienia się lokalizacja, aktualizujemy stan Redux
11 dispatch({
12 type: 'navigation/locationChanged',
13 payload: {
14 pathname: location.pathname,
15 search: location.search,
16 hash: location.hash,
17 state: location.state
18 }
19 });
20 }, [location, dispatch]);
21
22 return null; // Komponent nie renderuje niczego
23}
24
25// Użycie w App:
26function App() {
27 return (
28 <Router>
29 <HistorySync />
30 {/* Reszta aplikacji */}
31 </Router>
32 );
33}Często potrzebujemy analizować parametry zapytania z URL:
1import { useLocation } from 'react-router-dom';
2
3function useParsedQuery() {
4 const location = useLocation();
5 const queryParams = new URLSearchParams(location.search);
6
7 return {
8 get: (param) => queryParams.get(param),
9 has: (param) => queryParams.has(param),
10 getAll: (param) => queryParams.getAll(param),
11 toString: () => queryParams.toString(),
12 entries: () => Array.from(queryParams.entries()).reduce((acc, [key, value]) => {
13 acc[key] = value;
14 return acc;
15 }, {})
16 };
17}
18
19// Użycie:
20function SpaceshipFilter() {
21 const query = useParsedQuery();
22 const typeFilter = query.get('type'); // np. 'exploration'
23 const minCrewSize = query.get('minCrew'); // np. '3'
24
25 return (
26 <div>
27 <h2>Filtrowanie Statków Kosmicznych</h2>
28 {typeFilter && <p>Filtrowanie wg typu: {typeFilter}</p>}
29 {minCrewSize && <p>Minimalna wielkość załogi: {minCrewSize}</p>}
30
31 {/* Reszta komponentu */}
32 </div>
33 );
34}Czasami chcemy sprawdzić, jak głęboko w historii przeglądarki znajduje się użytkownik, lub wykonać akcję po osiągnięciu określonej głębokości:
1import { useState, useEffect } from 'react';
2
3function useHistoryDepth() {
4 const [depth, setDepth] = useState(0);
5
6 useEffect(() => {
7 // Ustaw początkową głębokość
8 setDepth(window.history.length);
9
10 // Nasłuchuj na zmiany
11 const handleNavigation = () => {
12 setDepth(window.history.length);
13 };
14
15 window.addEventListener('popstate', handleNavigation);
16
17 return () => {
18 window.removeEventListener('popstate', handleNavigation);
19 };
20 }, []);
21
22 return depth;
23}
24
25// Użycie:
26function NavigationDepthIndicator() {
27 const depth = useHistoryDepth();
28
29 return (
30 <div className="depth-indicator">
31 <p>Głębokość historii: {depth}</p>
32 {depth > 10 && (
33 <button onClick={() => window.history.go(-5)}>
34 Powrót o 5 stron
35 </button>
36 )}
37 </div>
38 );
39}Standardowe zachowanie przeglądarki to przywracanie pozycji przewijania przy nawigacji wstecz. W aplikacjach SPA musimy często implementować to ręcznie:
1import { useEffect, useRef } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useScrollRestoration() {
5 const location = useLocation();
6 const scrollPositions = useRef({});
7
8 // Zapisz pozycję przewijania przy zmianie lokalizacji
9 useEffect(() => {
10 const saveScrollPosition = (key) => {
11 scrollPositions.current[key] = window.scrollY;
12 };
13
14 // Zapisz bieżącą pozycję, gdy użytkownik opuszcza stronę
15 return () => {
16 saveScrollPosition(location.key);
17 };
18 }, [location]);
19
20 // Przywróć pozycję przewijania, gdy użytkownik powraca do strony
21 useEffect(() => {
22 if (scrollPositions.current[location.key] !== undefined) {
23 // Przywróć zapisaną pozycję
24 window.scrollTo(0, scrollPositions.current[location.key]);
25 } else {
26 // Nowa strona, przewiń do góry
27 window.scrollTo(0, 0);
28 }
29 }, [location]);
30}
31
32// Użycie:
33function ScrollRestoringPage() {
34 useScrollRestoration();
35
36 return (
37 <div className="long-content-page">
38 {/* Dużo treści powodującej przewijanie */}
39 </div>
40 );
41}Czasami potrzebujemy wiedzieć, czy zmieniły się konkretne parametry w URL:
1import { useEffect, useRef } from 'react';
2import { useLocation } from 'react-router-dom';
3
4function useQueryParamChange(paramName) {
5 const location = useLocation();
6 const previousParam = useRef(null);
7
8 // Pobierz aktualną wartość parametru
9 const query = new URLSearchParams(location.search);
10 const currentValue = query.get(paramName);
11
12 // Sprawdź, czy wartość się zmieniła
13 const hasChanged = previousParam.current !== currentValue;
14
15 // Aktualizuj ref dla następnego renderowania
16 useEffect(() => {
17 previousParam.current = currentValue;
18 }, [currentValue]);
19
20 return {
21 value: currentValue,
22 hasChanged,
23 previousValue: previousParam.current
24 };
25}
26
27// Użycie:
28function GalaxyFilter() {
29 const { value: galaxyType, hasChanged } = useQueryParamChange('galaxy');
30
31 useEffect(() => {
32 if (hasChanged) {
33 // Wykonaj akcję przy zmianie typu galaktyki
34 console.log(`Zmieniono filtr na: ${galaxyType}`);
35 // np. pobierz nowe dane
36 }
37 }, [galaxyType, hasChanged]);
38
39 return (
40 <div>
41 {/* Komponenty filtrowania */}
42 </div>
43 );
44}Świadome zarządzanie historią przeglądarki jest jak prowadzenie dziennika podróży kosmicznej - pozwala nam na:
Dobrze zaprojektowana obsługa historii przeglądarki sprawia, że podróż przez naszą aplikację jest intuicyjna i przyjemna, podobnie jak dobrze zaplanowana misja kosmiczna.
Pamiętaj, że historia przeglądarki to nie tylko mechanizm nawigacyjny, ale również ważny element doświadczenia użytkownika. Użytkownicy oczekują, że przyciski "Wstecz" i "Dalej" będą działać w sposób intuicyjny, a Twoja aplikacja powinna to respektować.