Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Nawigacja programistyczna w kosmicznych podróżach

W centrum dowodzenia statkiem kosmicznym potrzebujemy czasem nawigować między różnymi lokalizacjami nie tylko przy pomocy przycisków i linków, ale również programowo - na podstawie określonych warunków, zdarzeń lub logiki aplikacji. React Router dostarcza zaawansowane narzędzia do takiej programistycznej nawigacji, które pozwalają sterować trasą użytkownika z poziomu kodu.

Wprowadzenie do nawigacji programistycznej

Nawigacja programistyczna to technika zmiany adresu URL i renderowanego widoku za pomocą kodu JavaScript, bez bezpośredniej interakcji użytkownika z linkami czy przyciskami. Jest to niezbędny element wielu scenariuszy w aplikacjach:

  • Przekierowanie po pomyślnym zalogowaniu
  • Nawigacja po zakończeniu wieloetapowego formularza
  • Powrót po zapisaniu danych
  • Przekierowanie po upływie czasu sesji
  • Nawigacja na podstawie odpowiedzi z API

W React Router głównym narzędziem do nawigacji programistycznej jest hook

useNavigate
.

Hook useNavigate

Hook

useNavigate
wprowadzony w React Router v6 dostarcza funkcję, która pozwala programowo nawigować do innych ścieżek w aplikacji:

1import { useNavigate } from 'react-router-dom';
2
3function LaunchControlPanel() {
4  const navigate = useNavigate();
5  
6  const handleMissionStart = () => {
7    // Logika rozpoczęcia misji...
8    console.log('Misja rozpoczęta!');
9    
10    // Po rozpoczęciu misji, przekieruj do panelu monitorowania
11    navigate('/mission/monitoring');
12  };
13  
14  return (
15    <div className="launch-control">
16      <h2>Panel kontroli startu</h2>
17      <button 
18        className="launch-button" 
19        onClick={handleMissionStart}
20      >
21        Rozpocznij misję
22      </button>
23    </div>
24  );
25}

Podstawowe użycie navigate

Funkcja

navigate
może przyjmować różne argumenty:

  1. Ścieżkę docelową jako string:
1// Nawigacja do konkretnej ścieżki
2navigate('/planets/mars');
3
4// Nawigacja względna (o jeden poziom w górę)
5navigate('..');
6
7// Nawigacja względna (do podścieżki)
8navigate('systems');
  1. Liczbę (delta) dla nawigacji w historii:
1// Cofnij się o jedną stronę (jak przycisk "wstecz" w przeglądarce)
2navigate(-1);
3
4// Idź do przodu o jedną stronę (jak przycisk "do przodu" w przeglądarce)
5navigate(1);
6
7// Cofnij się o dwie strony
8navigate(-2);
  1. Obiekt z opcjami:
1// Nawigacja z opcjami
2navigate('/planets/mars', {
3  replace: true,  // Zastępuje bieżący wpis w historii zamiast dodawać nowy
4  state: { prevLocation: '/dashboard' }  // Dane stanu do przekazania
5});
6
7// Nawigacja z parametrami wyszukiwania
8navigate({
9  pathname: '/planets',
10  search: '?type=rocky&habitable=true'
11});

Opcja replace

Domyślnie,

navigate
dodaje nowy wpis do historii przeglądarki. Oznacza to, że użytkownik może wrócić do poprzedniej strony za pomocą przycisku "wstecz". Czasami jednak chcemy zastąpić bieżący wpis, aby użytkownik nie mógł wrócić (np. po wylogowaniu):

1function LogoutButton() {
2  const navigate = useNavigate();
3  
4  const handleLogout = () => {
5    // Wyloguj użytkownika
6    logout();
7    
8    // Przekieruj do strony logowania, zastępując bieżący wpis w historii
9    navigate('/login', { replace: true });
10  };
11  
12  return <button onClick={handleLogout}>Wyloguj</button>;
13}

Przekazywanie stanu przez nawigację

Możemy przekazać dane stanu do nowej lokalizacji za pomocą opcji

state
. Te dane nie są widoczne w URL, ale są dostępne w komponencie docelowym:

1// Komponent źródłowy
2function PlanetsList() {
3  const navigate = useNavigate();
4  
5  const viewPlanetDetails = (planet) => {
6    navigate(`/planets/${planet.id}`, {
7      state: { 
8        planetData: planet,
9        viewedFrom: 'planetsList',
10        timestamp: Date.now()
11      }
12    });
13  };
14  
15  return (
16    <div>
17      {planets.map(planet => (
18        <div key={planet.id} onClick={() => viewPlanetDetails(planet)}>
19          {planet.name}
20        </div>
21      ))}
22    </div>
23  );
24}
25
26// Komponent docelowy
27function PlanetDetails() {
28  const { state } = useLocation();
29  const { planetId } = useParams();
30  
31  useEffect(() => {
32    if (state?.viewedFrom) {
33      console.log(`Przekierowano z: ${state.viewedFrom}`);
34      console.log(`Dane planety z nawigacji: ${state.planetData}`);
35    }
36  }, [state]);
37  
38  return (
39    <div>
40      <h1>Szczegóły planety: {planetId}</h1>
41      {/* Reszta komponentu */}
42    </div>
43  );
44}

Budowanie zaawansowanych mechanizmów nawigacji

Wykorzystując hook

useNavigate
, możemy budować zaawansowane mechanizmy nawigacji, które obsługują złożone przepływy użytkownika:

1. Warunkowe przekierowania

1function ResourceLoader({ resourceId, resourceType }) {
2  const navigate = useNavigate();
3  const [resource, setResource] = useState(null);
4  const [error, setError] = useState(null);
5  
6  useEffect(() => {
7    fetchResource(resourceId, resourceType)
8      .then(data => {
9        setResource(data);
10        
11        // Warunkowo przekieruj na podstawie typu zasobu
12        if (resourceType === 'mission' && data.status === 'in-progress') {
13          navigate(`/mission-control/${resourceId}`);
14        } else if (resourceType === 'planet' && data.isRestricted) {
15          navigate('/access-denied', { 
16            replace: true,
17            state: { reason: 'restricted-planet' }
18          });
19        }
20      })
21      .catch(err => {
22        setError(err);
23        
24        // Przekieruj do strony błędu w przypadku problemu
25        navigate('/error', { 
26          state: { 
27            error: err.message,
28            resourceId,
29            resourceType
30          }
31        });
32      });
33  }, [resourceId, resourceType, navigate]);
34  
35  if (error) return <p>Ładowanie...</p>;
36  if (!resource) return <p>Ładowanie...</p>;
37  
38  return <ResourceViewer resource={resource} />;
39}

2. Sterowany przepływ wieloetapowy

1function MissionPlanner() {
2  const navigate = useNavigate();
3  const [currentStep, setCurrentStep] = useState(1);
4  const [missionData, setMissionData] = useState({});
5  
6  const handleStepComplete = (stepData) => {
7    // Uaktualnij dane misji o dane z bieżącego kroku
8    const updatedData = { ...missionData, ...stepData };
9    setMissionData(updatedData);
10    
11    if (currentStep < 3) {
12      // Przejdź do następnego kroku
13      setCurrentStep(currentStep + 1);
14    } else {
15      // Wszystkie kroki zakończone, zapisz misję i przekieruj
16      saveMission(updatedData)
17        .then(mission => {
18          navigate(`/missions/${mission.id}/overview`, {
19            state: { missionData: mission, isNewMission: true }
20          });
21        })
22        .catch(error => {
23          navigate('/error', { state: { error } });
24        });
25    }
26  };
27  
28  const handleCancel = () => {
29    // Zapytaj użytkownika, czy na pewno chce anulować
30    if (window.confirm('Czy na pewno chcesz anulować planowanie misji?')) {
31      navigate('/missions');
32    }
33  };
34  
35  return (
36    <div className="mission-planner">
37      <h1>Planowanie nowej misji</h1>
38      <ProgressBar currentStep={currentStep} totalSteps={3} />
39      
40      {currentStep === 1 && (
41        <MissionBasicInfoStep 
42          onComplete={handleStepComplete}
43          initialData={missionData}
44        />
45      )}
46      
47      {currentStep === 2 && (
48        <MissionCrewSelectionStep 
49          onComplete={handleStepComplete}
50          initialData={missionData}
51        />
52      )}
53      
54      {currentStep === 3 && (
55        <MissionConfirmationStep 
56          onComplete={handleStepComplete}
57          initialData={missionData}
58        />
59      )}
60      
61      <div className="actions">
62        {currentStep > 1 && (
63          <button onClick={() => setCurrentStep(currentStep - 1)}>
64            Wstecz
65          </button>
66        )}
67        
68        <button onClick={handleCancel}>Anuluj</button>
69      </div>
70    </div>
71  );
72}

3. Przekierowania oparte na czasie

1function SessionTimer() {
2  const navigate = useNavigate();
3  const [timeLeft, setTimeLeft] = useState(300); // 5 minut
4  
5  useEffect(() => {
6    if (timeLeft <= 0) {
7      // Sesja wygasła, przekieruj do strony logowania
8      alert('Twoja sesja wygasła. Zaloguj się ponownie.');
9      navigate('/login', { 
10        replace: true,
11        state: { sessionExpired: true }
12      });
13      return;
14    }
15    
16    // Uaktualnij timer co sekundę
17    const timer = setTimeout(() => {
18      setTimeLeft(timeLeft - 1);
19    }, 1000);
20    
21    return () => clearTimeout(timer);
22  }, [timeLeft, navigate]);
23  
24  const formatTime = () => {
25    const minutes = Math.floor(timeLeft / 60);
26    const seconds = timeLeft % 60;
27    return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
28  };
29  
30  return (
31    <div className="session-timer">
32      <span>Pozostały czas sesji: {formatTime()}</span>
33    </div>
34  );
35}

Nawigacja na podstawie odpowiedzi API

Bardzo powszechnym przypadkiem użycia nawigacji programistycznej jest przekierowanie na podstawie odpowiedzi z API. Oto kilka typowych scenariuszy:

1. Przekierowanie po pomyślnym przesłaniu formularza

1function NewPlanetForm() {
2  const navigate = useNavigate();
3  const [isSubmitting, setIsSubmitting] = useState(false);
4  const [error, setError] = useState(null);
5  
6  const handleSubmit = async (event) => {
7    event.preventDefault();
8    setIsSubmitting(true);
9    setError(null);
10    
11    try {
12      const formData = new FormData(event.target);
13      const planetData = {
14        name: formData.get('name'),
15        type: formData.get('type'),
16        diameter: Number(formData.get('diameter')),
17        hasWater: formData.get('hasWater') === 'on'
18      };
19      
20      // Wyślij dane do API
21      const response = await fetch('/api/planets', {
22        method: 'POST',
23        headers: {
24          'Content-Type': 'application/json'
25        },
26        body: JSON.stringify(planetData)
27      });
28      
29      if (!response.ok) {
30        throw new Error('Błąd podczas dodawania planety');
31      }
32      
33      const newPlanet = await response.json();
34      
35      // Przekieruj do strony szczegółów nowej planety
36      navigate(`/planets/${newPlanet.id}`, {
37        state: { 
38          message: 'Planeta została pomyślnie dodana!',
39          planetData: newPlanet
40        }
41      });
42    } catch (err) {
43      setError(err.message);
44      setIsSubmitting(false);
45    }
46  };
47  
48  return (
49    <form onSubmit={handleSubmit}>
50      {/* Pola formularza */}
51      {error && <div className="error">{error}</div>}
52      <button type="submit" disabled={isSubmitting}>
53        {isSubmitting ? 'Dodawanie...' : 'Dodaj planetę'}
54      </button>
55    </form>
56  );
57}

2. Przekierowanie po pomyślnym logowaniu

1function LoginForm() {
2  const navigate = useNavigate();
3  const location = useLocation();
4  
5  // Sprawdź, czy użytkownik został przekierowany z innej strony
6  const from = location.state?.from?.pathname || '/dashboard';
7  
8  const handleSubmit = async (event) => {
9    event.preventDefault();
10    const formData = new FormData(event.target);
11    
12    try {
13      // Wywołaj API logowania
14      const user = await loginUser(
15        formData.get('username'),
16        formData.get('password')
17      );
18      
19      // Zapisz dane użytkownika w stanie aplikacji
20      setUserInContext(user);
21      
22      // Przekieruj do pierwotnej lokalizacji lub dashboardu
23      navigate(from, { replace: true });
24    } catch (error) {
25      // Obsłuż błąd logowania
26      //
27    }
28  };
29  
30  return (
31    <form onSubmit={handleSubmit}>
32      {/* Pola formularza logowania */}
33    </form>
34  );
35}

Strażnicy tras (Route Guards)

Wykorzystując

useNavigate
, możemy tworzyć strażników tras, którzy automatycznie przekierowują użytkownika, jeśli nie spełnia określonych warunków. Ta technika jest często używana do zabezpieczania tras wymagających uwierzytelnienia:

1function RequireAuth({ children }) {
2  const navigate = useNavigate();
3  const location = useLocation();
4  const { user } = useAuth();
5  
6  useEffect(() => {
7    if (!user) {
8      // Przekieruj do strony logowania, zachowując informację,
9      // skąd użytkownik przyszedł, aby móc go tam wrócić po zalogowaniu
10      navigate('/login', {
11        replace: true,
12        state: { from: location }
13      });
14    }
15  }, [user, navigate, location]);
16  
17  // Jeśli użytkownik jest zalogowany, renderuj dzieci (chronioną trasę)
18  return user ? children : null;
19}
20
21// Użycie w definicji tras
22<Route 
23  path="/mission-control" 
24  element={
25    <RequireAuth>
26      <MissionControlPanel />
27    </RequireAuth>
28  } 
29/>

Nawigacja z parametrami wyszukiwania

Możemy użyć

navigate
do nawigowania z parametrami wyszukiwania, co jest przydatne dla filtrowania, sortowania i paginacji:

1function GalaxiesExplorer() {
2  const navigate = useNavigate();
3  const [searchParams] = useSearchParams();
4  
5  // Odczytaj aktualne filtry z URL
6  const currentType = searchParams.get('type') || 'all';
7  const currentSort = searchParams.get('sort') || 'name';
8  
9  const applyFilters = (filters) => {
10    // Stwórz nowy obiekt URLSearchParams
11    const params = new URLSearchParams();
12    
13    // Dodaj filtry do parametrów
14    if (filters.type && filters.type !== 'all') {
15      params.set('type', filters.type);
16    }
17    
18    if (filters.sort) {
19      params.set('sort', filters.sort);
20    }
21    
22    // Nawiguj do tej samej ścieżki, ale z nowymi parametrami
23    navigate({ 
24      pathname: '/galaxies',
25      search: params.toString() ? `?${params.toString()}` : ''
26    });
27  };
28  
29  return (
30    <div className="galaxies-explorer">
31      <div className="filters">
32        <label>
33          Typ galaktyki:
34          <select 
35            value={currentType}
36            onChange={(e) => applyFilters({ 
37              type: e.target.value, 
38              sort: currentSort 
39            })}
40          >
41            <option value="all">Wszystkie</option>
42            <option value="spiral">Spiralne</option>
43            <option value="elliptical">Eliptyczne</option>
44            <option value="irregular">Nieregularne</option>
45          </select>
46        </label>
47        
48        <label>
49          Sortuj wg:
50          <select 
51            value={currentSort}
52            onChange={(e) => applyFilters({ 
53              type: currentType, 
54              sort: e.target.value 
55            })}
56          >
57            <option value="name">Nazwa</option>
58            <option value="size">Rozmiar</option>
59            <option value="distance">Odległość</option>
60          </select>
61        </label>
62      </div>
63      
64      {/* Lista galaktyk */}
65    </div>
66  );
67}

Użycie navigate z generatorem URL

Możemy połączyć

navigate
z funkcją
createSearchParams
dla łatwiejszego tworzenia parametrów wyszukiwania:

1import { useNavigate, createSearchParams } from 'react-router-dom';
2
3function PlanetFilters() {
4  const navigate = useNavigate();
5  
6  const applyFilters = (filters) => {
7    // Użyj createSearchParams do wygenerowania string'a z parametrami
8    const params = createSearchParams({
9      type: filters.type,
10      hasWater: filters.hasWater ? 'true' : undefined,
11      minDiameter: filters.minDiameter,
12      sortBy: filters.sortBy
13    });
14    
15    navigate({
16      pathname: '/planets',
17      search: `?${params}`
18    });
19  };
20  
21  return (
22    <form onSubmit={(e) => {
23      e.preventDefault();
24      applyFilters({
25        type: e.target.type.value,
26        hasWater: e.target.hasWater.checked,
27        minDiameter: e.target.minDiameter.value,
28        sortBy: e.target.sortBy.value
29      });
30    }}>
31      {/* Pola formularza filtrów */}
32      <button type="submit">Zastosuj filtry</button>
33    </form>
34  );
35}

Problemy i pułapki związane z nawigacją programistyczną

1. Przekierowania podczas renderowania

Unikaj bezpośredniego używania

navigate
podczas renderowania komponentu - powoduje to problemy z wielokrotnym renderowaniem. Zamiast tego, używaj
navigate
w efektach, funkcjach obsługi zdarzeń lub callbackach:

1// ŹLE - powoduje nieskończoną pętlę renderowania
2function BadComponent() {
3  const navigate = useNavigate();
4  
5  if (!userIsAuthenticated) {
6    navigate('/login'); // To zostanie wywołane podczas każdego renderowania!
7  }
8  
9  return <div>Chroniona zawartość</div>;
10}
11
12// DOBRZE - używa useEffect
13function GoodComponent() {
14  const navigate = useNavigate();
15  
16  useEffect(() => {
17    if (!userIsAuthenticated) {
18      navigate('/login');
19    }
20  }, [navigate, userIsAuthenticated]);
21  
22  return <div>Chroniona zawartość</div>;
23}

2. Zagubione dane stanu

Pamiętaj, że dane przekazywane przez

navigate
w
state
są gubione przy odświeżeniu strony. Dla danych, które muszą przetrwać odświeżenie, używaj parametrów URL lub przechowuj je w lokalnym magazynie:

1// Dane zostaną utracone przy odświeżeniu
2navigate('/dashboard', { state: { importantData: data } });
3
4// Lepiej dla ważnych danych
5// 1. Użyj parametrów URL
6navigate(`/dashboard?dataId=${dataId}`);
7
8// 2. Lub zapisz w localStorage
9localStorage.setItem('importantData', JSON.stringify(data));
10navigate('/dashboard');

3. Problemy z asynchronicznymi przekierowaniami

Czasami chcemy przekierować użytkownika po zakończeniu operacji asynchronicznej. Uważaj na potencjalne problemy, gdy komponent zostanie odmontowany przed zakończeniem operacji:

1function AsyncNavigation() {
2  const navigate = useNavigate();
3  const [isLoading, setIsLoading] = useState(false);
4  
5  const handleClick = async () => {
6    setIsLoading(true);
7    
8    try {
9      // Operacja asynchroniczna
10      await someAsyncOperation();
11      
12      // Komponent może być już odmontowany w tym momencie!
13      // Jeśli używasz React 18 z Strict Mode, może to powodować problemy
14      navigate('/success');
15    } catch (error) {
16      setIsLoading(false);
17      // Obsłuż błąd
18    }
19  };
20  
21  return (
22    <button onClick={handleClick} disabled={isLoading}>
23      {isLoading ? 'Ładowanie...' : 'Wykonaj operację'}
24    </button>
25  );
26}

Rozwiązaniem jest użycie flag do śledzenia, czy komponent jest nadal zamontowany:

1function SafeAsyncNavigation() {
2  const navigate = useNavigate();
3  const [isLoading, setIsLoading] = useState(false);
4  const isMounted = useRef(true);
5  
6  useEffect(() => {
7    return () => {
8      // Oznacz komponent jako odmontowany przy czyszczeniu
9      isMounted.current = false;
10    };
11  }, []);
12  
13  const handleClick = async () => {
14    setIsLoading(true);
15    
16    try {
17      await someAsyncOperation();
18      
19      // Sprawdź, czy komponent jest nadal zamontowany
20      if (isMounted.current) {
21        navigate('/success');
22      }
23    } catch (error) {
24      if (isMounted.current) {
25        setIsLoading(false);
26      }
27    }
28  };
29  
30  return (
31    <button onClick={handleClick} disabled={isLoading}>
32      {isLoading ? 'Ładowanie...' : 'Wykonaj operację'}
33    </button>
34  );
35}

Podsumowanie

Nawigacja programistyczna jest potężnym narzędziem w arsenale developera React, pozwalającym na tworzenie zaawansowanych przepływów użytkownika i reagowanie na różne zdarzenia w aplikacji. Hook

useNavigate
oferuje elastyczne i intuicyjne API do kontrolowania nawigacji:

  1. Podstawowe przekierowania - proste przejścia między ścieżkami
  2. Nawigacja z parametrami - przekazywanie danych przez URL
  3. Przekazywanie stanu - niewidoczne dane dostępne na docelowej stronie
  4. Zastępowanie historii - kontrola nad stosem historii przeglądarki
  5. Warunkowe przekierowania - nawigacja oparta na logice biznesowej i odpowiedziach API

Używając nawigacji programistycznej, możesz budować bardziej dynamiczne i reagujące aplikacje, które płynnie przeprowadzają użytkownika przez różne kosmiczne przestrzenie twojej aplikacji.

Ir a CodeWorlds