Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Obsługa historii przeglądarki w podróży przez kosmiczny czas

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

Podstawy historii przeglądarki

Historia przeglądarki to stos odwiedzonych przez użytkownika adresów URL. Przeglądarki zapewniają API, które pozwala na:

  • Przechodzenie wstecz i wprzód między stronami
  • Dodawanie nowych wpisów do historii
  • Zastępowanie bieżącego wpisu
  • Manipulowanie stanem historii

React Router integruje się z tym API, dostarczając hooki i komponenty do zarządzania historią w sposób przyjazny dla Reacta.

Hook useNavigate

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}

Zastępowanie vs. dodawanie w historii

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
?

  1. Przekierowania po logowaniu/wylogowaniu - nie chcemy, aby użytkownik mógł wrócić do strony logowania po zalogowaniu
  2. Formularze wieloetapowe - nie chcemy, aby użytkownik mógł wrócić do pośrednich kroków procesu
  3. Przekierowania z nieistniejących stron - gdy przekierowujemy z 404 do strony głównej
  4. Kiedy realizujemy przekierowanie - aby nie powodować pętli w historii przeglądarki

Blokowanie nawigacji

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

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

Śledzenie historii nawigacji

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}

Modyfikowanie zachowania przycisku "Wstecz"

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}

Synchronizacja historii z globalnym stanem

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}

Analizowanie parametrów URL

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}

History Stack i zarządzanie głębokością

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}

Zaawansowane scenariusze

1. Przywracanie przewijania

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}

2. Wykrywanie zmiany parametrów

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}

Podsumowanie

Świadome zarządzanie historią przeglądarki jest jak prowadzenie dziennika podróży kosmicznej - pozwala nam na:

  1. Nawigację w czasie - użytkownik może łatwo powracać do odwiedzonych lokalizacji
  2. Kontrolę przepływu - możemy wpływać na to, jak użytkownik porusza się po aplikacji
  3. Zachowanie kontekstu - dane stanu mogą być przenoszone między lokalizacjami
  4. Optymalizację doświadczenia - przywracanie stanu, pozycji przewijania itp.

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

Vai a CodeWorlds