Démo 8 : Exercice 2
En règle général, les composants devraient recevoir leurs données en paramètres pour maximiser leur lisibilité et réutilisabilité. Cependant, pour des applications structurellement complexe, ces données doivent descendre un arbre profond et traverser des composants qui ne les utilise pas. C’est le problème que nous traitons ici.
Point de départ
Pour cet exercice, nous partons d’une application simple qui affiche, en haut à droite, l’utilisateur connecté et, s’il est administrateur, un lien vers l’administration. L’information part de la racine et doit atteindre deux petits composants profonds.
Fichier src/App.jsx
import Layout from "./components/Layout.jsx";
import AccueilPage from "./pages/AccueilPage.jsx";
// Utilisateur connecté simulé.
const utilisateurConnecte = { id: "u1", name: "Alex", role: "admin" };
export default function App() {
return (
<Layout user={utilisateurConnecte}>
<AccueilPage />
</Layout>
);
}Fichier src/components/Layout.jsx
import Header from "./Header.jsx";
export default function Layout({ user, children }) {
return (
<div className="app">
<Header user={user} />
<main>{children}</main>
</div>
);
}Fichier src/components/Header.jsx
import UserMenu from "./UserMenu.jsx";
export default function Header({ user }) {
return (
<header className="header">
<strong>Bus en vue</strong>
<UserMenu user={user} />
</header>
);
}Fichier src/components/UserMenu.jsx
import UserBadge from "./UserBadge.jsx";
import AdminLink from "./AdminLink.jsx";
export default function UserMenu({ user }) {
return (
<nav className="menu">
<AdminLink user={user} />
<UserBadge user={user} />
</nav>
);
}⭐ Fichier src/components/UserBadge.jsx
export default function UserBadge({ user }) {
return (
<span>
{user.name} <em>({user.role})</em>
</span>
);
}⭐ Fichier src/components/AdminLink.jsx
export default function AdminLink({ user }) {
if (user.role !== "admin") return null;
return <a href="#admin">Administration</a>;
}Pour transmettre user à UserBadge et AdminLink, il faut traverser Layout, Header et UserMenu,
qui ne font que transporter la donnée sans jamais la lire.
Le diagnostic
Encore une fois le code fonctionne, mais mérite tout de même d’être réusiner.
-
Des composants transportent une donnée qui ne les concerne pas.
Layoutmet en page,Headercompose une entête,UserMenugroupe des éléments. Aucun des trois n’a besoin de savoir qu’il existe un utilisateur, et pourtant tous les trois le savent. Chacun en sait plus que nécessaire pour faire son travail. -
Le cout d’un changement est proportionnel à la profondeur. Ajouter une fonction de déconnexion, renommer un champ, remplacer l’objet par autre chose : il faut ouvrir et modifier tous les niveaux traversés. Le nombre de fichiers touchés ne dépend pas de l’ampleur du changement, mais de la longueur du chemin.
-
Les intermédiaires perdent leur réutilisabilité.
Layoutexige maintenant unuser. Pour le réutiliser sur une page publique (sans utilisateur), il vous faudra inventer une valeur, ou rendre la prop facultative et traiter le cas partout. Une contrainte accidentelle s’est glissée dans son contrat. -
Le point de vérité est implicite. Rien, dans le code, ne dit qui possède l’utilisateur connecté ni comment on y accède. La réponse, « on se le passe de main en main », est une convention orale, pas une structure.
1. Mettre une donnée partagée à disposition sans friction
Problème
Il faut un moyen de mettre une donnée à la disposition d’un sous-arbre, sans la faire passer par chaque nœud.
La tentation serait d’en faire une variable globale.
// À ne pas faire.
export let utilisateurCourant = { id: "u1", name: "Alex", role: "admin" };Cependant, on perdrait ainsi le contrôle sur l’accès à la donnée. On aurait résolu la transmission en supprimant la portée.
Résolution
Ce que nous voulons est un intermédiaire : une valeur disponible dans une portée délimitée, que seul les composants situés dans cette portée peuvent demander.
C’est exactement le rôle de createContext en React et de son Provider.
Fichier src/context/UserContext.js
import { createContext } from "react";
export const UserContext = createContext(undefined);Fichier src/context/UserProvider.jsx
import { useState } from "react";
import { UserContext } from "./UserContext.js";
const UTILISATEUR_DEMO = { id: "u1", name: "Alex", role: "admin" };
export default function UserProvider({ children }) {
const [user] = useState(UTILISATEUR_DEMO);
// Depuis React 19, le contexte se rend directement comme fournisseur.
return <UserContext value={{ user }}>{children}</UserContext>;
}La portée, en React, c’est l’arbre lui-même : tout ce qui est rendu sous le fournisseur peut lire la valeur, et rien au-dessus.
Les intermédiaires (Layout, Header et UserMenu) sont ainsi déchargés de la tache de transmission de données.
Utilisation du contexte
Une fois la valeur fournie, un composant peut la lire directement :
import { use } from "react";
import { UserContext } from "../context/UserContext.js";
export default function UserBadge() {
const { user } = use(UserContext);
return <span>{user.name} <em>({user.role})</em></span>;
}Optimisation additionnelle
Fichier src/hooks/useUser.js
import { use } from "react";
import { UserContext } from "../context/UserContext.js";
export function useUser() {
const contexte = use(UserContext);
if (contexte === undefined) {
throw new Error("useUser doit être utilisé à l'intérieur d'un <UserProvider>.");
}
return contexte;
}Avantages. Un seul point d’accès, une seule erreur possible, et un message qui nomme la cause. Les composants consommateurs ne connaissent plus ni use ni UserContext : ils appellent useUser. Le jour où l’implémentation change, ils ne s’en apercevront pas.
Désavantages. C’est un fichier de plus pour trois lignes, et une indirection supplémentaire à suivre quand on lit le code. La discipline repose de surcroit sur une convention : rien n’empêche d’importer UserContext et de le lire directement, en court-circuitant la garde.
2. La valeur transmise : stabilité et évolution
Le problème
Regardez à nouveau le fournisseur écrit plus haut :
<UserContext value={{ user }}>Cet objet { user } est reconstruit à chaque rendu du fournisseur. Même si user n’a pas changé d’un iota, la valeur du contexte est une nouvelle référence, et tous les consommateurs se redessinent. Le contexte compare les valeurs par identité, pas par contenu : il ne sait pas que le nouvel objet dit la même chose que l’ancien.
Se pose aussi une question de conception : pourquoi transmettre { user } plutôt que user directement ?
Résolution
Le principe est celui de l’immuabilité et de la comparaison par identité. On ne modifie pas la valeur, on la remplace, et l’on ne la remplace que lorsqu’elle change réellement. Les consommateurs peuvent alors se fier à une simple comparaison de référence pour savoir s’ils doivent réagir.
useMemo remplit exactement ce rôle : il conserve la même référence tant que ses dépendances n’ont pas changé.
Fichier src/context/UserProvider.jsx
import { useMemo, useState } from "react";
import { UserContext } from "./UserContext.js";
const UTILISATEUR_DEMO = { id: "u1", name: "Alex", role: "admin" };
export default function UserProvider({ children }) {
const [user] = useState(UTILISATEUR_DEMO);
// La valeur reste la même référence tant que `user` ne change pas.
const valeur = useMemo(() => ({ user }), [user]);
return <UserContext value={valeur}>{children}</UserContext>;
}Quant à la forme de la valeur, transmettre { user } plutôt que user est un choix délibéré. À l’exercice 3, ce contexte devra aussi exposer login, logout et un indicateur de chargement. Si les consommateurs écrivaient aujourd’hui const user = useUser(), il faudrait tous les réécrire. En écrivant dès maintenant const { user } = useUser(), l’ajout de nouvelles clés ne cassera personne.
Avantages. Une ligne (useMemo) supprime une classe entière de redessins inutiles. Et la forme en objet rend le contrat extensible sans rupture.
Désavantages. C’est une subtilité que rien ne signale : le code sans useMemo fonctionne parfaitement, il est simplement plus lent, et le problème ne devient visible qu’à l’échelle. Il faut donc le savoir. Par ailleurs, useMemo a lui-même un cout (mémoriser, comparer les dépendances), négligeable ici, mais qui rappelle que l’optimisation n’est jamais gratuite.
3. La chaine dénouée, et ses limites
Le problème
Il reste à récolter le bénéfice : retirer user des trois composants qui ne s’en servaient pas, et laisser les deux consommateurs aller le chercher eux-mêmes.
En React
Fichier src/App.jsx
import UserProvider from "./context/UserProvider.jsx";
import Layout from "./components/Layout.jsx";
import AccueilPage from "./pages/AccueilPage.jsx";
export default function App() {
return (
<UserProvider>
<Layout>
<AccueilPage />
</Layout>
</UserProvider>
);
}Fichier src/components/Layout.jsx
import Header from "./Header.jsx";
export default function Layout({ children }) {
return (
<div className="app">
<Header />
<main>{children}</main>
</div>
);
}Header et UserMenu subissent exactement le même traitement : la prop disparait de leur signature, et ils cessent de la transmettre. Plus une seule mention de user dans ces trois fichiers : ils sont redevenus ce qu’ils prétendaient être.
Les deux consommateurs, eux, lisent le contexte. Ils passent ici par le hook useUser de l’étape précédente ; si vous avez choisi de vous en passer, remplacez simplement useUser() par use(UserContext).
Fichier src/components/UserBadge.jsx
import { useUser } from "../hooks/useUser.js";
export default function UserBadge() {
const { user } = useUser();
return (
<span>
{user.name} <em>({user.role})</em>
</span>
);
}Fichier src/components/AdminLink.jsx
import { useUser } from "../hooks/useUser.js";
export default function AdminLink() {
const { user } = useUser();
if (user.role !== "admin") return null;
return <a href="#admin">Administration</a>;
}Ce que le contexte ne résout pas
Il faut nommer tout de suite la limite, car elle décide de la suite du cours.
Un contexte notifie tous ses consommateurs à chaque changement de valeur, sans distinction. Il n’existe pas de moyen de s’abonner à une partie seulement de la valeur.
C’est sans conséquence ici : l’utilisateur connecté change une fois à la connexion, une fois à la déconnexion. Ce sera tout autre chose lorsqu’il faudra partager un état qui change souvent et qui est lu par morceaux, comme des lignes favorites : chaque clic sur une étoile redessinerait alors tous les consommateurs du contexte, y compris ceux que le changement ne concerne pas. C’est précisément ce qui motivera un store, à l’exercice 5.
Deux autres limites méritent d’être connues :
- La dépendance implicite, déjà évoquée : un composant qui appelle
useUserne peut plus être monté n’importe où, et sa signature n’en dit rien. - La tentation du contexte fourre-tout : il est tentant de tout mettre dans un seul contexte « application ». C’est le meilleur moyen de faire redessiner l’écran entier pour un changement mineur. Mieux vaut plusieurs contextes ciblés qu’un seul contexte omniscient.
VérificationFaut-il désormais préférer le contexte aux props ?
Non. Les props restent le mécanisme par défaut : elles rendent la dépendance visible dans la signature, et le flux facile à suivre. Le contexte se justifie quand une donnée est partagée par des composants dispersés dans l’arbre, et qu’elle change rarement. Passer une donnée à un enfant direct, ou même à deux niveaux, ne justifie pas un contexte : cela ajouterait de l’indirection sans rien résoudre.
Récapitulatif
L’arborescence à l’arrivée. Trois fichiers ont été ajoutés, et trois composants ont été allégés.
src/
├── context/
│ ├── UserContext.js création du contexte, sans JSX
│ └── UserProvider.jsx la valeur, mémoïsée, et la portée
├── hooks/
│ └── useUser.js l'accès unique, avec sa garde
└── components/
├── Layout.jsx ne connait plus l'utilisateur
├── Header.jsx ne connait plus l'utilisateur
├── UserMenu.jsx ne connait plus l'utilisateur
├── UserBadge.jsx consomme le contexte
└── AdminLink.jsx consomme le contexteLe prix payé se résume en deux points, qu’il faut avoir en tête avant d’en abuser : la dépendance devient implicite, et tout changement redessine tous les consommateurs. Le premier point est un compromis assumé. Le second sera le point de départ de l’exercice 5.
Enfin, notez que le fournisseur est désormais le point d’ancrage de la session : il tient l’utilisateur, aujourd’hui simulé. À l’exercice 3, il tiendra un utilisateur bien réel et exposera login et logout, sans qu’aucun consommateur écrit ici ne change d’une ligne.
Pour vous entrainer hors du fil « Bus en vue », reprenez le même geste sur une donnée différente :
une chaine de props theme (avec sa fonction de changement) traversant une barre d’outils,
à migrer vers un ThemeContext dont la valeur sera { theme, setTheme }. Vous y retrouverez la forme exacte que prendra le contexte utilisateur à l’exercice suivant.