Aller au contenu

a

React-router

Les exercices de cette septième partie du cours diffèrent un peu de ceux d'avant. Dans ce chapitre et le suivant, comme d'habitude, il y a des exercices liés à la théorie du chapitre.

En plus des exercices de ce chapitre et du suivant, il y a une série d'exercices dans lesquels nous réviserons ce que nous avons appris durant tout le cours en élargissant l'application Bloglist sur laquelle nous avons travaillé pendant les parties 4 et 5.

Structure de navigation de l'application

Après la partie 6, nous revenons à React sans Redux.

Il est très courant que les applications web aient une barre de navigation, qui permet de changer la vue de l'application.

Notre application pourrait avoir une page principale

navigateur montrant l'app de notes avec le lien de navigation home

et des pages séparées pour afficher des informations sur les notes et les utilisateurs:

navigateur montrant l'app de notes avec le lien de navigation notes

Dans une application web à l'ancienne, changer la page affichée par l'application serait accompli par le navigateur effectuant une requête HTTP GET au serveur et rendant le HTML représentant la vue qui a été retournée.

Dans les applications à page unique, nous sommes, en réalité, toujours sur la même page. Le code Javascript exécuté par le navigateur crée une illusion de différentes "pages". Si des requêtes HTTP sont faites lors du changement de vues, elles sont uniquement pour récupérer des données formatées en JSON, dont la nouvelle vue pourrait avoir besoin pour être affichée.

La barre de navigation et une application contenant plusieurs vues sont très faciles à implémenter en utilisant React.

Voici une manière de le faire:

import { useState }  from 'react'
import ReactDOM from 'react-dom/client'

const Home = () => (
  <div> <h2>TKTL notes app</h2> </div>
)

const Notes = () => (
  <div> <h2>Notes</h2> </div>
)

const Users = () => (
  <div> <h2>Users</h2> </div>
)

const App = () => {
  const [page, setPage] = useState('home')

  const toPage = (page) => (event) => {
    event.preventDefault()
    setPage(page)
  }

  const content = () => {
    if (page === 'home') {
      return <Home />
    } else if (page === 'notes') {
      return <Notes />
    } else if (page === 'users') {
      return <Users />
    }
  }

  const padding = {
    padding: 5
  }

  return (
    <div>
      <div>
        <a href="" onClick={toPage('home')} style={padding}>
          home
        </a>
        <a href="" onClick={toPage('notes')} style={padding}>
          notes
        </a>
        <a href="" onClick={toPage('users')} style={padding}>
          users
        </a>
      </div>

      {content()}
    </div>
  )
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

Chaque vue est implémentée comme son propre composant. Nous stockons les informations du composant de vue dans l'état de l'application appelé page. Ces informations nous indiquent quel composant, représentant une vue, devrait être affiché sous la barre de menu.

Cependant, la méthode n'est pas très optimale. Comme nous pouvons le voir sur les images, l'adresse reste la même même si parfois nous sommes dans des vues différentes. Chaque vue devrait de préférence avoir sa propre adresse, par exemple pour rendre possible l'ajout aux favoris. Le bouton retour ne fonctionne pas comme prévu pour notre application non plus, signifiant que retour ne vous déplace pas vers la vue précédemment affichée de l'application, mais quelque part complètement différent. Si l'application devait grandir encore plus et que nous voulions, par exemple, ajouter des vues séparées pour chaque utilisateur et note, alors ce routing fait maison, qui signifie la gestion de la navigation de l'application, deviendrait excessivement compliqué.

React Router

Heureusement, React dispose de la bibliothèque React Router qui fournit une excellente solution pour gérer la navigation dans une application React.

Changeons l'application ci-dessus pour utiliser React Router. D'abord, nous installons React Router avec la commande

npm install react-router-dom

Le routage fourni par React Router est activé en modifiant l'application comme suit:

import {
  BrowserRouter as Router,
  Routes, Route, Link
} from 'react-router-dom'

const App = () => {

  const padding = {
    padding: 5
  }

  return (
    <Router>
      <div>
        <Link style={padding} to="/">home</Link>
        <Link style={padding} to="/notes">notes</Link>
        <Link style={padding} to="/users">users</Link>
      </div>

      <Routes>
        <Route path="/notes" element={<Notes />} />
        <Route path="/users" element={<Users />} />
        <Route path="/" element={<Home />} />
      </Routes>

      <div>
        <i>Note app, Department of Computer Science 2023</i>
      </div>
    </Router>
  )
}

Le routage, ou le rendu conditionnel de composants en fonction de l'URL dans le navigateur, est utilisé en plaçant les composants comme enfants du composant Router, c'est-à-dire à l'intérieur des balises Router.

Notez que, même si le composant est désigné par le nom Router, nous parlons de BrowserRouter, car ici l'importation se fait en renommant l'objet importé:

import {
  BrowserRouter as Router,  Routes, Route, Link
} from 'react-router-dom'

Selon la documentation v5:

BrowserRouter est un Router qui utilise l'API d'historique HTML5 (pushState, replaceState et l'événement popState) pour maintenir votre UI synchronisée avec l'URL.

Normalement, le navigateur charge une nouvelle page lorsque l'URL dans la barre d'adresse change. Cependant, avec l'aide de l'API d'historique HTML5, BrowserRouter nous permet d'utiliser l'URL dans la barre d'adresse du navigateur pour le "routage" interne dans une application React. Ainsi, même si l'URL dans la barre d'adresse change, le contenu de la page est uniquement manipulé en utilisant Javascript, et le navigateur ne chargera pas de nouveau contenu depuis le serveur. L'utilisation des actions retour et avance, ainsi que la création de signets, reste logique comme sur une page web traditionnelle.

À l'intérieur du routeur, nous définissons des liens qui modifient la barre d'adresse avec l'aide du composant Link. Par exemple,

<Link to="/notes">notes</Link>

crée un lien dans l'application avec le texte notes, qui, lorsqu'il est cliqué, change l'URL dans la barre d'adresse en /notes.

Les composants rendus en fonction de l'URL du navigateur sont définis avec l'aide du composant Route. Par exemple,

<Route path="/notes" element={<Notes />} />

définit que, si l'adresse du navigateur est /notes, nous rendons le composant Notes.

Nous enveloppons les composants à rendre en fonction de l'URL avec un composant Routes

<Routes>
  <Route path="/notes" element={<Notes />} />
  <Route path="/users" element={<Users />} />
  <Route path="/" element={<Home />} />
</Routes>

Routes fonctionne en rendant le premier composant dont le path correspond à l'URL dans la barre d'adresse du navigateur.

Route paramétrée

Examinons une version légèrement modifiée de l'exemple précédent. Le code complet de l'exemple mis à jour peut être trouvé ici.

L'application contient maintenant cinq vues différentes dont l'affichage est contrôlé par le routeur. En plus des composants de l'exemple précédent (Home, Notes et Users), nous avons Login représentant la vue de connexion et Note représentant la vue d'une note unique.

Home et Users sont inchangés par rapport à l'exercice précédent. Notes est un peu plus compliqué. Il rend la liste des notes qui lui sont passées en props de manière à ce que le nom de chaque note soit cliquable.

application de notes montrant que les notes sont cliquables

La possibilité de cliquer sur un nom est mise en oeuvre avec le composant Link, et cliquer sur le nom d'une note dont l'id est 3 déclencherait un événement qui change l'adresse du navigateur en notes/3:

const Notes = ({notes}) => (
  <div>
    <h2>Notes</h2>
    <ul>
      {notes.map(note =>
        <li key={note.id}>
          <Link to={`/notes/${note.id}`}>{note.content}</Link>        </li>
      )}
    </ul>
  </div>
)

Nous définissons des URL paramétrées dans le routage dans le composant App comme suit:

<Router>
  // ...

  <Routes>
    <Route path="/notes/:id" element={<Note notes={notes} />} />    <Route path="/notes" element={<Notes notes={notes} />} />   
    <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
    <Route path="/login" element={<Login onLogin={login} />} />
    <Route path="/" element={<Home />} />      
  </Routes>
</Router>

Nous définissons la route qui rend une note spécifique "à la manière d'Express" en marquant le paramètre avec deux points - :id

<Route path="/notes/:id" element={<Note notes={notes} />} />

Lorsqu'un navigateur navigue vers l'URL d'une note spécifique, par exemple, /notes/3, nous rendons le composant Note:

import {
  // ...
  useParams} from 'react-router-dom'

const Note = ({ notes }) => {
  const id = useParams().id  const note = notes.find(n => n.id === Number(id)) 
  return (
    <div>
      <h2>{note.content}</h2>
      <div>{note.user}</div>
      <div><strong>{note.important ? 'important' : ''}</strong></div>
    </div>
  )
}

Le composant Note reçoit toutes les notes en tant que props notes, et il peut accéder au paramètre de l'URL (l'id de la note à afficher) avec la fonction useParams de React Router.

useNavigate

Nous avons également implémenté une fonction de connexion simple dans notre application. Si un utilisateur est connecté, les informations concernant un utilisateur connecté sont sauvegardées dans le champ user de l'état du composant App.

L'option de naviguer vers la vue Login est rendue conditionnellement dans le menu.

<Router>
  <div>
    <Link style={padding} to="/">home</Link>
    <Link style={padding} to="/notes">notes</Link>
    <Link style={padding} to="/users">users</Link>
    {user      ? <em>{user} logged in</em>      : <Link style={padding} to="/login">login</Link>    }  </div>

  // ...
</Router>

Donc, si l'utilisateur est déjà connecté, au lieu d'afficher le lien Login, nous montrons le nom d'utilisateur:

application de notes du navigateur montrant le nom d'utilisateur connecté)

Le code du composant gérant la fonctionnalité de connexion est le suivant:

import {
  // ...
  useNavigate} from 'react-router-dom'

const Login = (props) => {
  const navigate = useNavigate()
  const onSubmit = (event) => {
    event.preventDefault()
    props.onLogin('mluukkai')
    navigate('/')  }

  return (
    <div>
      <h2>login</h2>
      <form onSubmit={onSubmit}>
        <div>
          username: <input />
        </div>
        <div>
          password: <input type='password' />
        </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

Ce qui est intéressant à propos de ce composant est l'utilisation de la fonction useNavigate de React Router. Avec cette fonction, l'URL du navigateur peut être modifiée de manière programmatique.

Lors de la connexion de l'utilisateur, nous appelons navigate('/') ce qui provoque le changement de l'URL du navigateur en / et l'application rend le composant correspondant Home.

Les fonctions useParams et useNavigate sont des fonctions de hook, tout comme useState et useEffect que nous avons maintenant utilisées de nombreuses fois. Comme vous vous en souvenez de la partie 1, il existe certaines règles à respecter lors de l'utilisation des fonctions de hook. Create-react-app a été configuré pour vous avertir si vous enfreignez ces règles, par exemple, en appelant une fonction de hook depuis une instruction conditionnelle.

redirect

Il y a un autre détail intéressant concernant la route Users:

<Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />

Si un utilisateur n'est pas connecté, le composant Users n'est pas rendu. À la place, l'utilisateur est redirigé en utilisant le composant Navigate vers la vue de connexion:

<Navigate replace to="/login" />

En réalité, il serait peut-être préférable de ne même pas afficher dans la barre de navigation les liens nécessitant une connexion si l'utilisateur n'est pas connecté à l'application.

Voici le composant App dans son intégralité:

const App = () => {
  const [notes, setNotes] = useState([
    // ...
  ])

  const [user, setUser] = useState(null) 

  const login = (user) => {
    setUser(user)
  }

  const padding = {
    padding: 5
  }

  return (
    <div>
      <Router>
        <div>
          <Link style={padding} to="/">home</Link>
          <Link style={padding} to="/notes">notes</Link>
          <Link style={padding} to="/users">users</Link>
          {user
            ? <em>{user} logged in</em>
            : <Link style={padding} to="/login">login</Link>
          }
        </div>

        <Routes>
          <Route path="/notes/:id" element={<Note notes={notes} />} />  
          <Route path="/notes" element={<Notes notes={notes} />} />   
          <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
          <Route path="/login" element={<Login onLogin={login} />} />
          <Route path="/" element={<Home />} />      
        </Routes>
      </Router>      
      <footer>
        <br />
        <em>Note app, Department of Computer Science 2023</em>
      </footer>
    </div>
  )
}

Nous définissons un élément commun aux applications web modernes appelé footer, qui définit la partie en bas de l'écran, en dehors du Router, de sorte qu'elle soit affichée indépendamment du composant montré dans la partie routée de l'application.

Revisite de la route paramétrée

Notre application a un défaut. Le composant Note reçoit toutes les notes, même s'il n'affiche que celle dont l'identifiant correspond au paramètre de l'URL:

const Note = ({ notes }) => { 
  const id = useParams().id
  const note = notes.find(n => n.id === Number(id))
  // ...
}

Serait-il possible de modifier l'application de sorte que le composant Note ne reçoive que la note qu'il doit afficher?

const Note = ({ note }) => {
  return (
    <div>
      <h2>{note.content}</h2>
      <div>{note.user}</div>
      <div><strong>{note.important ? 'important' : ''}</strong></div>
    </div>
  )
}

Une façon de faire cela serait d'utiliser le hook useMatch de React Router pour déterminer l'identifiant de la note à afficher dans le composant App.

Il n'est pas possible d'utiliser le hook useMatch dans le composant qui définit la partie routée de l'application. Déplaçons l'utilisation des composants Router depuis App:

ReactDOM.createRoot(document.getElementById('root')).render(
  <Router>    <App />
  </Router>)

Le composant App devient:

import {
  // ...
  useMatch} from 'react-router-dom'

const App = () => {
  // ...

  const match = useMatch('/notes/:id')  const note = match     ? notes.find(note => note.id === Number(match.params.id))    : null
  return (
    <div>
      <div>
        <Link style={padding} to="/">home</Link>
        // ...
      </div>

      <Routes>
        <Route path="/notes/:id" element={<Note note={note} />} />        <Route path="/notes" element={<Notes notes={notes} />} />   
        <Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
        <Route path="/login" element={<Login onLogin={login} />} />
        <Route path="/" element={<Home />} />      
      </Routes>   

      <div>
        <em>Note app, Department of Computer Science 2023</em>
      </div>
    </div>
  )
}  

Chaque fois que le composant est rendu, donc pratiquement chaque fois que l'URL du navigateur change, la commande suivante est exécutée:

const match = useMatch('/notes/:id')

Si l'URL correspond à /notes/:id, la variable match contiendra un objet à partir duquel nous pouvons accéder à la partie paramétrée du chemin, l'identifiant de la note à afficher, et nous pouvons ensuite récupérer la note correcte à afficher.

const note = match 
  ? notes.find(note => note.id === Number(match.params.id))
  : null

Le code complet peut être trouvé ici.