React Router

revoir l’épisode précédent

Au clic

Dans une application React, il est souvent nécessaire d’effectuer des actions spécifiques au moment du chargement initial de la page. Cette étape peut être cruciale pour initialiser des données, récupérer des informations à partir de services externes…

Repartons sur notre page Home :

function Home() {
  return <h1>Hello from Home</h1>;
}

export default Home;

Modifions le code pour exécuter des instructions (la fonction getWeatherOfTheDay dans cet exemple) au clic d’un bouton :

import { useState } from "react";

const getWeatherOfTheDay = () => {
  return "sunny";
};

function Home() {
  const [weather, setWeather] = useState(null as string | null);

  return (
    <>
      <h1>Hello from Home</h1>

      {weather != null ? (
        <p>Today is a {weather} day</p>
      ) : (
        <button
          type="button"
          onClick={() => {
            const weatherOfTheDay = getWeatherOfTheDay();

            setWeather(weatherOfTheDay);
          }}
        >
          Get Weather
        </button>
      )}
    </>
  );
}

export default Home;

La fonction getWeatherOfTheDay retourne directement une chaine de caractères. Elle pourrait faire des opérations plus complexes comme appeler une API par exemple. Ce n’est pas qui ce nous intéresse aujourd’hui : ce qui nous intéresse à ce stade c’est nous les récupérons les données au moment du clic.

Dans cette version, nous sommes limités par les événements du DOM : il ne se passera rien tant que la personne qui visite la page n’aura pas cliqué sur le bouton. Parfois (voir même souvent…), tu auras besoin de déclencher du code dès le “démarrage” de ta page. Une approche courante pour effectuer des instructions au démarrage de la page consiste à utiliser le hook useEffect.

Au démarrage

Dans l’exemple ci-dessus, nous avons introduit une fonction getWeatherOfTheDay qui peut être utilisée pour récupérer les informations météo. Plutôt que d’attendre une interaction de l’utilisateur, nous pouvons utiliser useEffect pour exécuter cette fonction automatiquement :

import { useEffect, useState } from "react";

const getWeatherOfTheDay = () => {
  return "sunny";
};

function Home() {
  const [weather, setWeather] = useState(null as string | null);

  useEffect(() => {
    const weatherOfTheDay = getWeatherOfTheDay();

    setWeather(weatherOfTheDay);
  }, []);

  return (
    <>
      <h1>Hello from Home</h1>

      {weather != null && <p>Today is a {weather} day</p>}
    </>
  );
}

export default Home;

Voici comment cela fonctionne :

L’action est toujours exécutée après un premier rendu “à vide”.

L’utilisation de useEffect est idéale pour effectuer des actions au démarrage, mais React Router apporte un concurrent sérieux : les loaders.

Le plus tôt possible

Une autre approche pour exécuter des instructions au démarrage de la page est d’utiliser React Router avec des loaders. Les loaders sont des fonctions spéciales que tu peux associer à des routes pour charger des données avant le rendu de la page. Cette méthode permet d’obtenir des données dès que possible, ce qui peut être essentiel pour une expérience utilisateur fluide.

Pour illustrer cette approche, reprenons notre route Home dans main.tsx:

// ...

const getWeatherOfTheDay = () => {
  return "sunny";
};

// router creation

const router = createBrowserRouter([
  {
    element: <App />,
    children: [
      {
        path: "/",
        element: <Home />,
        loader: () => {
          return getWeatherOfTheDay();
        },
      },
      // ...
    ],
  },
]);

// ...

Dans ce cas, nous utilisons un loader pour charger les données météo avant le rendu de la page. Voici comment cela fonctionne :

Dans le composant Home :

import { useLoaderData } from "react-router-dom";

function Home() {
  const weather = useLoaderData() as string;

  return (
    <>
      <h1>Hello from Home</h1>
      <p>Today is a {weather} day</p>
    </>
  );
}

export default Home;

Nous utilisons le hook useLoaderData pour récupérer les données du loader. Cela garantit que les données sont disponibles dès le rendu de la page (plus besoin de rendu conditionnel pour éviter une valeur nulle au premier affichage).

Une alternative intéressante est d’utiliser un loader “global”. Au lieu d’associer le loader directement à la route de la page, nous pouvons définir un loader au niveau de l’application (note aussi l’ajout d’un id sur la route de l’application) :

// ...

// router creation

const router = createBrowserRouter([
  {
    element: <App />,
    loader: () => {
      return getWeatherOfTheDay();
    },
    id: "app",
    children: [
      {
        path: "/",
        element: <Home />,
      },
      // ...
    ],
  },
]);

// ...

Cela signifie que les données sont chargées dès le démarrage de l’application et sont disponibles pour toutes les pages. Les données chargées globalement peuvent être accessibles dans n’importe quelle partie de l’application, et nous pouvons les récupérer à l’aide du hook useRouteLoaderData en spécifiant l’identifiant du loader ciblé (dans ce cas, "app") :

import { useRouteLoaderData } from "react-router-dom";

function Home() {
  const weather = useRouteLoaderData("app") as string;

  return (
    <>
      <h1>Hello from Home</h1>
      <p>Today is a {weather} day</p>
    </>
  );
}

export default Home;

Cette approche des loaders est puissante car elle permet de charger des données importantes en amont, ce qui peut améliorer considérablement les performances de l’application et l’expérience utilisateur. Cela garantit également que les données sont disponibles dès que possible, ce qui est particulièrement important pour les applications nécessitant des données initiales avant tout rendu.

Recharger la page

Lorsqu’il s’agit de mettre à jour des données ou d’effectuer des actions en réponse à des changements dans l’application, React offre plusieurs approches. L’utilisation de useEffect et les loaders de React Router sont là encore 2 options très intéressantes.

Revenons cette fois sur notre page Article :

import { useParams } from "react-router-dom";

function Article() {
  const { id } = useParams();

  return <h1>Hello from Article {id}</h1>;
}

export default Article;

Un moyen courant de gérer les mises à jour dans un composant React est d’utiliser le hook useEffect. Il permet d’effectuer des actions en réponse à des changements spécifiques, tels que des changements d’état ou des modifications de propriétés :

import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";

const allData = [
  {
    id: 1,
    title: "Lorem Ipsum",
    content: "Lorem ipsum dolor sit amet",
  },
  {
    id: 2,
    title: "Schnapsum",
    content: "Lorem Elsass ipsum Salut bisamme",
  },
  {
    id: 3,
    title: "Cupcake Ipsum",
    content: "Tiramisu pastry wafer brownie soufflé",
  },
];

type Data = typeof allData[0];

const getSomeData = (id: number) => {
  return allData.find((article) => article.id === id) as Data | null;
};

function Article() {
  const { id } = useParams();

  const [data, setData] = useState(null as Data | null);

  useEffect(() => {
    const idAsInt = parseInt(id ?? "0");

    const someData = getSomeData(idAsInt);

    setData(someData);
  }, [id]);

  return (
    data != null && (
      <article>
        <h1>{data.title}</h1>
        <p>{data.content}</p>
      </article>
    )
  );
}

export default Article;

Dans cet exemple, le composant Article affiche les détails d’un article en fonction de son identifiant. Le hook useParams est utilisé pour récupérer l’identifiant de l’article à partir de l’URL. Ensuite, nous utilisons le hook useEffect pour effectuer une action chaque fois que l’identifiant change.

Le fameux tableau de dépendances en deuxième argument de useEffect est utilisé pour surveiller les changements de id. Lorsque id change (par exemple, en naviguant vers un autre article), useEffect appelle getSomeData pour récupérer les nouvelles données et les affiche dans le composant.

Une autre approche pour gérer les mises à jour consiste à utiliser les loaders de React Router :

// ...

const allData = [
  {
    id: 1,
    title: "Lorem Ipsum",
    content: "Lorem ipsum dolor sit amet",
  },
  {
    id: 2,
    title: "Schnapsum",
    content: "Lorem Elsass ipsum Salut bisamme",
  },
  {
    id: 3,
    title: "Cupcake Ipsum",
    content: "Tiramisu pastry wafer brownie soufflé",
  },
];

type Data = typeof allData[0];

const getSomeData = (id: number) => {
  return allData.find((article) => article.id === id) as Data | null;
};

// router creation

const router = createBrowserRouter([
  {
    element: <App />,
    children: [
      // ...
      {
        path: "/articles/:id",
        element: <Article />,
        loader: ({ params }) => {
          const idAsInt = parseInt(params.id ?? "0");

          return getSomeData(idAsInt);
        },
      },
    ],
  },
]);

// ...

Dans cet exemple, un loader est associé à la route "/articles/:id". Le loader est déclenché automatiquement chaque fois qu’un utilisateur accède à une URL correspondant à cette route. De manière similaire aux props des composants, params est extrait du premier paramètre de la fonction, et nous donne accès à l’id dans l’URL.

Le résultat du loader (les données de l’article) est ensuite disponible pour le composant Article via le hook useLoaderData comme vu précédemment :

import { useLoaderData } from "react-router-dom";

type Data = {
    title: string;
    content: string;
}

function Article() {
  const data = useLoaderData() as Data;

  return (
    <article>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </article>
  );
}

export default Article;

useEffect vs loader

Maintenant que nous avons examiné deux approches pour gérer les mises à jour dans une application React, il est important de comprendre les avantages et les inconvénients de chaque méthode.

useEffect

Avantages :

Inconvénients :

Loaders de React Router

Avantages :

Inconvénients :

Le choix entre useEffect et les loaders dépend de ton cas d’utilisation spécifique. Le mieux reste de demander des conseils pour être sûr de toi.