Cours JavaScript et React · Module 6: React Router

La navigation programmatique dans les voyages cosmiques

12 min de lecture
Dans cette leçon9

Dans le centre de commandement du vaisseau, nous avons parfois besoin de naviguer entre différents emplacements non seulement à l'aide de boutons et de liens, mais aussi de manière programmatique - en fonction de conditions spécifiques, d'événements ou de la logique de l'application. React Router fournit des outils avancés pour ce type de navigation programmatique, te permettant de contrôler le parcours de l'utilisateur directement depuis le code.

Introduction à la navigation programmatique

La navigation programmatique est une technique qui consiste à changer l'adresse URL et la vue rendue à l'aide de code JavaScript, sans interaction directe de l'utilisateur avec des liens ou des boutons. C'est un élément essentiel de nombreux scénarios d'application :

  • Redirection après une connexion réussie
  • Navigation après avoir complété un formulaire en plusieurs étapes
  • Retour après l'enregistrement de données
  • Redirection après l'expiration de la session
  • Navigation basée sur les réponses d'une API

Dans React Router, le principal outil de navigation programmatique est le hook useNavigate.

Le hook useNavigate

Le hook useNavigate, introduit dans React Router v6, fournit une fonction qui permet de naviguer de manière programmatique vers d'autres chemins de l'application :

1import { useNavigate } from 'react-router-dom';
2
3function LaunchControlPanel() {
4  const navigate = useNavigate();
5
6  const handleMissionStart = () => {
7    // Logique de démarrage de la mission...
8    console.log('Mission démarrée !');
9
10    // Après le démarrage de la mission, rediriger vers le panneau de surveillance
11    navigate('/mission/monitoring');
12  };
13
14  return (
15    <div className="launch-control">
16      <h2>Panneau de contrôle du lancement</h2>
17      <button
18        className="launch-button"
19        onClick={handleMissionStart}
20      >
21        Démarrer la mission
22      </button>
23    </div>
24  );
25}

Utilisation de base de navigate

La fonction navigate peut accepter différents arguments :

  1. Le chemin cible sous forme de chaîne :
1// Naviguer vers un chemin spécifique
2navigate('/planets/mars');
3
4// Navigation relative (un niveau vers le haut)
5navigate('..');
6
7// Navigation relative (vers un sous-chemin)
8navigate('systems');
  1. Un nombre (delta) pour la navigation dans l'historique :
1// Reculer d'une page (comme le bouton "retour" du navigateur)
2navigate(-1);
3
4// Avancer d'une page (comme le bouton "suivant" du navigateur)
5navigate(1);
6
7// Reculer de deux pages
8navigate(-2);
  1. Un objet avec des options :
1// Navigation avec options
2navigate('/planets/mars', {
3  replace: true,  // Remplace l'entrée actuelle de l'historique au lieu d'en ajouter une nouvelle
4  state: { prevLocation: '/dashboard' }  // Données d'état à passer
5});
6
7// Navigation avec des paramètres de recherche
8navigate({
9  pathname: '/planets',
10  search: '?type=rocky&habitable=true'
11});

L'option replace

Par défaut, navigate ajoute une nouvelle entrée à l'historique du navigateur. Cela signifie que l'utilisateur peut revenir à la page précédente à l'aide du bouton "retour". Parfois, cependant, nous voulons remplacer l'entrée actuelle pour que l'utilisateur ne puisse pas revenir en arrière (par ex. après une déconnexion) :

1function LogoutButton() {
2  const navigate = useNavigate();
3
4  const handleLogout = () => {
5    // Déconnecter l'utilisateur
6    logout();
7
8    // Rediriger vers la page de connexion en remplaçant l'entrée actuelle dans l'historique
9    navigate('/login', { replace: true });
10  };
11
12  return <button onClick={handleLogout}>Déconnexion</button>;
13}

Passer un état via la navigation

Nous pouvons passer des données d'état vers un nouvel emplacement à l'aide de l'option state. Ces données ne sont pas visibles dans l'URL, mais sont disponibles dans le composant cible :

1// Composant source
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// Composant cible
27function PlanetDetails() {
28  const { state } = useLocation();
29  const { planetId } = useParams();
30
31  useEffect(() => {
32    if (state?.viewedFrom) {
33      console.log(`Redirigé depuis : ${state.viewedFrom}`);
34      console.log(`Données de la planète depuis la navigation : ${state.planetData}`);
35    }
36  }, [state]);
37
38  return (
39    <div>
40      <h1>Détails de la planète : {planetId}</h1>
41      {/* Reste du composant */}
42    </div>
43  );
44}

Construire des mécanismes de navigation avancés

À l'aide du hook useNavigate, nous pouvons construire des mécanismes de navigation avancés qui gèrent des flux utilisateur complexes :

1. Redirections conditionnelles

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        // Rediriger conditionnellement selon le type de ressource
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        // Rediriger vers la page d'erreur en cas de problème
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>Chargement...</p>;
36  if (!resource) return <p>Chargement...</p>;
37
38  return <ResourceViewer resource={resource} />;
39}

2. Flux multi-étapes contrôlé

1function MissionPlanner() {
2  const navigate = useNavigate();
3  const [currentStep, setCurrentStep] = useState(1);
4  const [missionData, setMissionData] = useState({});
5
6  const handleStepComplete = (stepData) => {
7    // Mettre à jour les données de la mission avec les données de l'étape actuelle
8    const updatedData = { ...missionData, ...stepData };
9    setMissionData(updatedData);
10
11    if (currentStep < 3) {
12      // Passer à l'étape suivante
13      setCurrentStep(currentStep + 1);
14    } else {
15      // Toutes les étapes terminées, enregistrer la mission et rediriger
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    // Demander à l'utilisateur s'il veut vraiment annuler
30    if (window.confirm('Es-tu sûr de vouloir annuler la planification de la mission ?')) {
31      navigate('/missions');
32    }
33  };
34
35  return (
36    <div className="mission-planner">
37      <h1>Planification d'une nouvelle mission</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            Retour
65          </button>
66        )}
67
68        <button onClick={handleCancel}>Annuler</button>
69      </div>
70    </div>
71  );
72}

3. Redirections basées sur le temps

1function SessionTimer() {
2  const navigate = useNavigate();
3  const [timeLeft, setTimeLeft] = useState(300); // 5 minutes
4
5  useEffect(() => {
6    if (timeLeft <= 0) {
7      // Session expirée, rediriger vers la page de connexion
8      alert('Ta session a expiré. Merci de te reconnecter.');
9      navigate('/login', {
10        replace: true,
11        state: { sessionExpired: true }
12      });
13      return;
14    }
15
16    // Mettre à jour le minuteur chaque seconde
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>Temps de session restant : {formatTime()}</span>
33    </div>
34  );
35}

Un cas d'usage très courant de la navigation programmatique est la redirection basée sur les réponses d'une API. Voici quelques scénarios typiques :

1. Redirection après un envoi de formulaire réussi

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      // Envoyer les données à l'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('Erreur lors de l\'ajout de la planète');
31      }
32
33      const newPlanet = await response.json();
34
35      // Rediriger vers la page de détails de la nouvelle planète
36      navigate(`/planets/${newPlanet.id}`, {
37        state: {
38          message: 'La planète a été ajoutée avec succès !',
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      {/* Champs du formulaire */}
51      {error && <div className="error">{error}</div>}
52      <button type="submit" disabled={isSubmitting}>
53        {isSubmitting ? 'Ajout...' : 'Ajouter la planète'}
54      </button>
55    </form>
56  );
57}

2. Redirection après une connexion réussie

1function LoginForm() {
2  const navigate = useNavigate();
3  const location = useLocation();
4
5  // Vérifier si l'utilisateur a été redirigé depuis une autre page
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      // Appeler l'API de connexion
14      const user = await loginUser(
15        formData.get('username'),
16        formData.get('password')
17      );
18
19      // Enregistrer les données de l'utilisateur dans l'état de l'application
20      setUserInContext(user);
21
22      // Rediriger vers l'emplacement d'origine ou le dashboard
23      navigate(from, { replace: true });
24    } catch (error) {
25      // Gérer l'erreur de connexion
26      //
27    }
28  };
29
30  return (
31    <form onSubmit={handleSubmit}>
32      {/* Champs du formulaire de connexion */}
33    </form>
34  );
35}

Les gardes de route

À l'aide de useNavigate, nous pouvons créer des gardes de route qui redirigent automatiquement l'utilisateur s'il ne remplit pas certaines conditions. Cette technique est souvent utilisée pour protéger les routes nécessitant une authentification :

1function RequireAuth({ children }) {
2  const navigate = useNavigate();
3  const location = useLocation();
4  const { user } = useAuth();
5
6  useEffect(() => {
7    if (!user) {
8      // Rediriger vers la page de connexion en conservant l'information
9      // sur l'origine de l'utilisateur pour pouvoir le ramener après la connexion
10      navigate('/login', {
11        replace: true,
12        state: { from: location }
13      });
14    }
15  }, [user, navigate, location]);
16
17  // Si l'utilisateur est connecté, rendre children (la route protégée)
18  return user ? children : null;
19}
20
21// Utilisation dans la définition de route
22<Route
23  path="/mission-control"
24  element={
25    <RequireAuth>
26      <MissionControlPanel />
27    </RequireAuth>
28  }
29/>

Nous pouvons utiliser navigate pour naviguer avec des paramètres de recherche, ce qui est utile pour le filtrage, le tri et la pagination :

1function GalaxiesExplorer() {
2  const navigate = useNavigate();
3  const [searchParams] = useSearchParams();
4
5  // Lire les filtres actuels depuis l'URL
6  const currentType = searchParams.get('type') || 'all';
7  const currentSort = searchParams.get('sort') || 'name';
8
9  const applyFilters = (filters) => {
10    // Créer un nouvel objet URLSearchParams
11    const params = new URLSearchParams();
12
13    // Ajouter les filtres aux paramètres
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    // Naviguer vers le même chemin mais avec de nouveaux paramètres
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          Type de galaxie :
34          <select
35            value={currentType}
36            onChange={(e) => applyFilters({
37              type: e.target.value,
38              sort: currentSort
39            })}
40          >
41            <option value="all">Toutes</option>
42            <option value="spiral">Spirale</option>
43            <option value="elliptical">Elliptique</option>
44            <option value="irregular">Irrégulière</option>
45          </select>
46        </label>
47
48        <label>
49          Trier par :
50          <select
51            value={currentSort}
52            onChange={(e) => applyFilters({
53              type: currentType,
54              sort: e.target.value
55            })}
56          >
57            <option value="name">Nom</option>
58            <option value="size">Taille</option>
59            <option value="distance">Distance</option>
60          </select>
61        </label>
62      </div>
63
64      {/* Liste des galaxies */}
65    </div>
66  );
67}

Utiliser navigate avec un générateur d'URL

Nous pouvons combiner navigate avec la fonction createSearchParams pour faciliter la création de paramètres de recherche :

1import { useNavigate, createSearchParams } from 'react-router-dom';
2
3function PlanetFilters() {
4  const navigate = useNavigate();
5
6  const applyFilters = (filters) => {
7    // Utiliser createSearchParams pour générer une chaîne de paramètres
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      {/* Champs du formulaire de filtres */}
32      <button type="submit">Appliquer les filtres</button>
33    </form>
34  );
35}

Problèmes et pièges de la navigation programmatique

1. Redirections pendant le rendu

Évite d'utiliser navigate directement pendant le rendu d'un composant - cela provoque des problèmes de re-rendus multiples. À la place, utilise navigate dans des effets, des gestionnaires d'événements ou des callbacks :

1// MAUVAIS - provoque une boucle de rendu infinie
2function BadComponent() {
3  const navigate = useNavigate();
4
5  if (!userIsAuthenticated) {
6    navigate('/login'); // Ceci sera appelé à chaque rendu !
7  }
8
9  return <div>Contenu protégé</div>;
10}
11
12// BON - utilise useEffect
13function GoodComponent() {
14  const navigate = useNavigate();
15
16  useEffect(() => {
17    if (!userIsAuthenticated) {
18      navigate('/login');
19    }
20  }, [navigate, userIsAuthenticated]);
21
22  return <div>Contenu protégé</div>;
23}

2. Données d'état perdues

Souviens-toi que les données passées via navigate dans state sont perdues lors d'un rafraîchissement de la page. Pour les données qui doivent survivre à un rafraîchissement, utilise les paramètres d'URL ou stocke-les dans le stockage local :

1// Les données seront perdues au rafraîchissement
2navigate('/dashboard', { state: { importantData: data } });
3
4// Mieux pour les données importantes
5// 1. Utiliser les paramètres d'URL
6navigate(`/dashboard?dataId=${dataId}`);
7
8// 2. Ou enregistrer dans localStorage
9localStorage.setItem('importantData', JSON.stringify(data));
10navigate('/dashboard');

3. Problèmes avec les redirections asynchrones

Parfois, nous voulons rediriger l'utilisateur après l'achèvement d'une opération asynchrone. Sois attentif aux problèmes potentiels lorsque le composant est démonté avant la fin de l'opération :

1function AsyncNavigation() {
2  const navigate = useNavigate();
3  const [isLoading, setIsLoading] = useState(false);
4
5  const handleClick = async () => {
6    setIsLoading(true);
7
8    try {
9      // Opération asynchrone
10      await someAsyncOperation();
11
12      // Le composant peut déjà être démonté à ce stade !
13      // Si tu utilises React 18 avec le mode strict, cela peut causer des problèmes
14      navigate('/success');
15    } catch (error) {
16      setIsLoading(false);
17      // Gérer l'erreur
18    }
19  };
20
21  return (
22    <button onClick={handleClick} disabled={isLoading}>
23      {isLoading ? 'Chargement...' : 'Exécuter l\'opération'}
24    </button>
25  );
26}

La solution consiste à utiliser des drapeaux pour suivre si le composant est toujours monté :

1function SafeAsyncNavigation() {
2  const navigate = useNavigate();
3  const [isLoading, setIsLoading] = useState(false);
4  const isMounted = useRef(true);
5
6  useEffect(() => {
7    return () => {
8      // Marquer le composant comme démonté lors du nettoyage
9      isMounted.current = false;
10    };
11  }, []);
12
13  const handleClick = async () => {
14    setIsLoading(true);
15
16    try {
17      await someAsyncOperation();
18
19      // Vérifier si le composant est toujours monté
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 ? 'Chargement...' : 'Exécuter l\'opération'}
33    </button>
34  );
35}

Résumé

La navigation programmatique est un outil puissant dans l'arsenal du développeur React, permettant de créer des flux utilisateur avancés et de réagir à divers événements de l'application. Le hook useNavigate offre une API flexible et intuitive pour contrôler la navigation :

  1. Redirections de base - transitions simples entre les chemins
  2. Navigation avec paramètres - passage de données via l'URL
  3. Passage d'état - données invisibles disponibles sur la page cible
  4. Remplacement de l'historique - contrôle sur la pile d'historique du navigateur
  5. Redirections conditionnelles - navigation basée sur la logique métier et les réponses d'API

En utilisant la navigation programmatique, tu peux construire des applications plus dynamiques et réactives qui guident l'utilisateur en douceur à travers les différents espaces cosmiques de ton application.

Code de cette leçon : App.jsx
1import React from 'react';
2import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
3
4// Navigation avec NavLink
5function Navigation() {
6  return (
7    <nav className="sidebar">
8      <h2>Station spatiale</h2>
9
10      <NavLink
11        to="/"
12        className={({ isActive }) => isActive ? 'nav-item active' : 'nav-item'}
13      >
14        Dashboard
15      </NavLink>
16
17      <NavLink
18        to="/crew"
19        className={({ isActive }) => isActive ? 'nav-item active' : 'nav-item'}
20      >
21        Équipage
22      </NavLink>
23
24      <NavLink
25        to="/missions"
26        className={({ isActive }) => isActive ? 'nav-item active' : 'nav-item'}
27      >
28        Missions
29      </NavLink>
30
31      <NavLink
32        to="/systems"
33        className={({ isActive }) => isActive ? 'nav-item active' : 'nav-item'}
34      >
35        Systèmes
36      </NavLink>
37
38      <NavLink
39        to="/comms"
40        style={({ isActive }) => ({
41          color: isActive ? '#ffd700' : '#ffa500',
42          fontWeight: isActive ? 'bold' : 'normal',
43          background: isActive ? 'rgba(255, 215, 0, 0.2)' : 'transparent',
44        })}
45        className="nav-item"
46      >
47        Communication
48      </NavLink>
49    </nav>
50  );
51}
52
53// Composants des pages
54function Dashboard() {
55  return (
56    <div className="page">
57      <h1>Dashboard</h1>
58      <p>Bienvenue dans le centre de contrôle de la station spatiale !</p>
59      <div className="stats">
60        <div className="stat"><span>Équipage :</span> 6 membres</div>
61        <div className="stat"><span>Missions :</span> 3 actives</div>
62        <div className="stat"><span>Statut :</span> Tout est OK</div>
63      </div>
64    </div>
65  );
66}
67
68function Crew() {
69  return (
70    <div className="page">
71      <h1>Équipage</h1>
72      <ul className="crew-list">
73        <li>Anna Kowalska - Commandante</li>
74        <li>Jan Nowak - Pilote</li>
75        <li>Maria Wisniewska - Ingénieure</li>
76        <li>Piotr Kowalczyk - Scientifique</li>
77      </ul>
78    </div>
79  );
80}
81
82function Missions() {
83  return (
84    <div className="page">
85      <h1>Missions</h1>
86      <div className="mission-list">
87        <div className="mission">Apollo 25 - En cours</div>
88        <div className="mission">Artemis 12 - Planification</div>
89        <div className="mission">Europa Probe - Test</div>
90      </div>
91    </div>
92  );
93}
94
95function Systems() {
96  return (
97    <div className="page">
98      <h1>Systèmes</h1>
99      <div className="systems-grid">
100        <div className="system ok">Alimentation :</div>
101        <div className="system ok">Survie :</div>
102        <div className="system ok">Navigation :</div>
103        <div className="system warn">Communication :</div>
104      </div>
105    </div>
106  );
107}
108
109function Comms() {
110  return (
111    <div className="page">
112      <h1>Communication</h1>
113      <p>Dernier contact avec la Terre : il y a 2 minutes</p>
114      <p>Puissance du signal : 87%</p>
115    </div>
116  );
117}
118
119function App() {
120  return (
121    <BrowserRouter>
122      <div className="app">
123        <Navigation />
124        <main>
125          <Routes>
126            <Route path="/" element={<Dashboard />} />
127            <Route path="/crew" element={<Crew />} />
128            <Route path="/missions" element={<Missions />} />
129            <Route path="/systems" element={<Systems />} />
130            <Route path="/comms" element={<Comms />} />
131          </Routes>
132        </main>
133      </div>
134    </BrowserRouter>
135  );
136}
137
138export default App;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel est le rôle du hook useNavigate dans React Router v6 ?

  2. 2. Le composant Outlet dans React Router sert à :

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Navigation programmatique

Articles utiles