Qu'est-ce qu'un layout en React ?

|

Image de couverture

Si vous êtes développeur React, vous savez probablement ce qu'est un layout.

Ou pas, car il s'avère que "layout" est l'un des termes les plus ambigus du développement web moderne en JavaScript.

Découvrons non pas une, ni deux, mais quatre (!) définitions de ce qu'est un layout dans React.

Première définition : du point de vue du composant

Commençons par une définition du layout en tant que type de composant React :

Dans React, un layout est un composant qui enveloppe plusieurs pages.

C'est tout.

Au fond, un layout est un composant React comme les autres. Ce qui le rend si spécial, c'est sa relation avec le routeur utilisé dans votre application, qui dépend de votre framework ou de votre stack technologique : le App Router de Next, React Router pour Remix etc.

De la même manière, les pages ne sont que des composants React normaux, mais associés à une URL.

Un layout dans React

Un exemple de layout pour mon blog. Un layout est un composant React qui enveloppe le contenu de la page.

Pour reformuler cette définition en termes de routage :

  • un layout existe sur plusieurs routes ("/home", "/blog"...)
  • une page est associée à une route ("/blog")
  • les autres composants n'ont aucune relation spéciale avec le routeur

Le cycle de vie des layouts, et aussi des pages, est contrôlé par le routeur lorsque l'utilisateur navigue entre différentes routes.

C'est pourquoi nous nous retrouvons avec des interprétations légèrement différentes de ce qu'est un layout, en fonction du framework utilisé.

Le fait que la navigation entre pages soit une navigation côté client (style SPA) plutôt qu'une navigation côté serveur (balises <a> classiques) est important pour comprendre comment le layout se comporte. Dans cet article nous parlons spécifiquement de la navigation côté client, en utilisant des composants <Link /> plutôt que des ancres <a> brutes.

Explorons trois technologies, React Router, Next.js et Astro, pour trouver des nouvelles définitions de "layout".

React Router : les layouts stateful

Dans React Router, lors de la navigation côté client, les layouts :

  • ne se "remontent" pas
  • se re-rendent uniquement si vous utilisez le hook useLocation

Étant donné que React Router est l'un des routeurs les plus anciens et les plus populaires, c'est ainsi que la plupart des développeurs React interpréteront les layouts.

Cette définition peut être reformulée en termes de "state" : les layouts sont des composants qui conservent leur état (pas de "remount") pendant la navigation côté client.

Avant les hooks, le layout React Router se re-rendait à chaque navigation. Aujourd'hui, il ne se re-rendra que si vous utilisez réellement un hook réactif qui dépend de l'URL actuelle, comme useLocation. Cela rend les layouts plus efficaces.

Si vous avez un state global dans un React Context, vous devrez définir le provider correspondant dans un layout.

En termes de code, un layout React Router ne prend aucune props, et rend un <Outlet /> qui correspond à ses enfants.

Je ne sais pas pourquoi React Router préfère un composant Outlet à la props children traditionnelle 🤷‍♂. Contactez-moi sur Twitter si vous avez plus d'infos !

Voici à quoi pourrait ressembler notre layout :

import { useState } from "react";
import { Outlet, Link, useLocation } from "react-router-dom";
// C'est juste un composant React
export default function Root() {
    // La valeur aléatoire restera la même 
    // pendant la navigation côté client
    const [randomState] = useState(Math.random())
    // Nous pouvons lire l'URL facilement
    let location = useLocation();
    // Se re-rendera pendant la navigation, 
    //à cause du hook useLocation
    console.log("Rendering Root")
    return (
        <>
            <div id="sidebar">
                <h1>React Router Contacts</h1>
                <div>
                Current pathname: {location.pathname}
                </div>
                <div>
                Current state: {randomState}
                </div>
                <div id="detail">
                    <Outlet />
                </div>

Nous configurons ensuite le routeur pour utiliser ce composant Root :

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "contacts/:contactId",
        element: <Contact />,
      },
    ],
  },
]);

Techniquement, nous devrions différencier les "routes imbriquées" des vraies "routes de layout" documentées ici.

Une route imbriquée est associée à un segment d'URL, comme "/account", et son composant agira en tant que layout pour les pages imbriquées comme "/account/profile" et "/account/update-password".

Une route de layout n'est pas associée à un segment d'URL, elle fournit juste le composant React qui enveloppe plusieurs pages.

Passons maintenant à Next.

Next.js App Router, les layouts super statiques

Dans Next.js, avec l'App Router et pendant la navigation côté client, les layouts :

  • ne se remontent pas
  • ne se re-rendent pas

Il est important de noter que par défaut, les layouts Next.js sont des React Server Components (RSC). Ils ne sont rendus qu'une fois sur le serveur et jamais côté client.

Next a une interprétation très statique des layouts, et très spécifiques aux Server Components.

En particulier, le fait qu'ils ne se re-rendent pas pendant la navigation côté client explique pourquoi vous ne pouvez pas accéder à l'URL dans les layouts.

Cette limitation peut sembler un peu sévère et est largement débattue sur le GitHub de Next.js. Cependant, notez que vous pouvez toujours utiliser un composant client pour le layout ou une partie de celui-ci et vous appuyer sur le hook usePathname.

Un layout React Router avec le hook useLocation de "react-router-dom" se comporte comme un layout Next.js avec la directive "use client" et le hook usePathname de "next/navigation". Oui, c'est compliqué un layout !

De plus, cette approche sans re-rendu fait que les layouts sont un endroit peu approprié pour effectuer des vérifications d'authentification pour du contenu payant ou privé.

La vérification ne sera pas effectuée à nouveau pendant la navigation côté client, et vous pouvez complètement contourner un layout pour accéder au contenu d'une page, comme expliqué dans mon précédent article sur les paywalls dans Next.js.

En termes d'implémentation, un layout est défini en utilisant un fichier spécial "layout.ts". Le composant reçoit une propriété children qui correspond à la page actuelle, ainsi que les paramètres de route actuels pour les routes dynamiques (comme "/blog/article-42").

// app/layout.ts
// Le layout racine pour ce blog,
// qui affiche les balises html et body
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <main>
        {/** La Navbar peut être un composant client 
        et peut accéder à l'URL */*/}
          <Navbar />
          {children}
          ...

Les pages héritent de la nature statique ou dynamique de leur layout parent. Mon espoir est que, dans le futur, le Prérendu Partiel (PPR) permettra de mélanger des layouts statiques et des pages dynamiques.

Pas si simple un layout dans le framework Next.js ! Découvrons un dernier type de layout, le layout Astro.

Astro, le layout visuel

Astro n'est pas exactement un framework React, mais étant donné qu'il est compatible avec de nombreuses bibliothèques d'UI dont React, je l'inclus dans ma liste.

Dans Astro, pendant la navigation côté client, les layouts :

  • se remontent
  • se re-rendent

Ils sont très dynamiques, tout l'inverse d'un layout Next.js !

Astro se conforme à une définition purement visuelle d'un layout, ou "mise en page" comme leur excellente traduction française l'indique. Un layout est un composant que vous utilisez dans plusieurs pages, mais il n'a aucun comportement spécial lorsqu'il s'agit de navigation utilisateur.

---
// src/pages/some-page.astro
// Vous devez importer le composant layout 
// dans chaque page
// c'est juste un composant React normal
import MySiteLayout 
    from '../layouts/MySiteLayout.astro';
---
<MySiteLayout title="Home Page">
  <p>
  Mon contenu de page, 
  enveloppé dans un layout !
  </p>
</MySiteLayout>

Dans Next.js, cela serait appelé un template, en utilisant "template.js" comme nom de fichier plutôt que "layout.js".

Donc oui, rien de spécial ici, dans Astro les layouts sont juste des composants React normaux, que vous utilisez dans plusieurs pages.

Ils ne conserveront pas l'état pendant la navigation côté client dans le cas où votre layout utilise une directive client.

Dans l'ancien "Pages Router" de Next.js (avant la v13), les layouts se comportaient aussi comme cela. Nous pouvions bidouiller un peu pour obtenir la persistance de l'état, mais c'était plutôt compliqué.

Conclusion : en React il n'y a pas un layout, mais des layouts

Le terme "layout" est très courant dans l'écosystème JavaScript, et pourtant, il est très ambigu.

Lorsqu'on parle de layouts dans React, quelques points devraient être clarifiés :

  • Le layout est-il un composant serveur React (React Server Component), ou un composant client traditionnel ?
  • Est-ce qu'il se remonte ou non pendant la navigation côté client ? S'il se remonte, il ne préservera pas l'état entre les pages.
  • Quand se rend-il à nouveau : à chaque navigation, uniquement si vous utilisez des hooks réactifs pour lire l'URL actuelle, ou uniquement si vous rechargez la page ?

Si vous n'êtes pas sûr, vous pouvez toujours observer le comportement du layout de votre framework préféré avec un useState contenant une variable aléatoire.

Merci d'avoir lu cet article, j'espère que vous avez maintenant une vision plus claire de ce que "layout" signifie, ou pas, dans React !