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.
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:
W React Router głównym narzędziem do nawigacji programistycznej jest 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}Funkcja
navigate może przyjmować różne argumenty: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// 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// 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});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}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}Wykorzystując hook
useNavigate, możemy budować zaawansowane mechanizmy nawigacji, które obsługują złożone przepływy użytkownika: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}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}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}Bardzo powszechnym przypadkiem użycia nawigacji programistycznej jest przekierowanie na podstawie odpowiedzi z API. Oto kilka typowych scenariuszy:
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}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}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/>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}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}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}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');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}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: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.