frontend / react
Context API
Explication
Ce que vous allez apprendre
- Comprendre le problème du prop-drilling et comment Context le résout
- Créer un contexte, fournir une valeur avec
Provideret la lire avecuseContext - Construire un Provider custom encapsulé dans un hook dédié (
useAuth) - Comprendre pourquoi tout changement de la valeur d'un Provider re-rend ses consommateurs
- Savoir quand Context devient insuffisant et qu'un state manager externe s'impose
Dans quel contexte ?
Dans une application avec App.jsx > Layout.jsx > Sidebar.jsx > UserMenu.jsx, l'information "utilisateur connecté" doit être disponible dans UserMenu, quatre niveaux plus bas. La faire transiter par prop à travers Layout et Sidebar, qui n'en ont eux-mêmes aucun usage, alourdit inutilement leur signature et rend le code fragile au moindre refactoring. Un AuthContext avec un AuthProvider en haut de l'arbre résout ce problème une bonne fois pour toutes.
Étape 1 : comment circulent les données normalement
Dans une application React, les données circulent naturellement des composants parents vers leurs enfants, via les props.
Étape 2 : le problème quand la profondeur augmente
Ce fonctionnement devient pénible quand une donnée doit atteindre un composant situé très profondément dans l'arbre : il faut la faire transiter à travers tous les composants intermédiaires, même ceux qui n'en ont aucun usage.
Étape 3 : la solution, Context
C'est ce qu'on appelle le "prop-drilling", et c'est précisément ce que Context est conçu pour résoudre : rendre une donnée disponible directement à n'importe quel composant descendant.
Étape 4 : l'image à retenir
On peut se représenter Context comme une diffusion : un composant Provider, placé en haut de l'arbre, diffuse une valeur que tous ses descendants peuvent capter, où qu'ils se trouvent.
Étape 5 : les trois étapes toujours présentes
Trois étapes structurent toujours l'usage de Context : créer le contexte avec createContext, fournir une valeur avec <MonContexte.Provider>, puis la lire avec useContext(MonContexte).
Étape 6 : un pattern répandu
Un pattern très répandu consiste à encapsuler ces trois étapes dans un composant Provider personnalisé et un hook custom dédié comme useAuth, avec une vérification claire si le hook est utilisé hors du Provider.
La limite à connaître
Il reste une limite importante : chaque changement de la valeur d'un Provider re-rend TOUS ses composants consommateurs, même ceux qui ne s'intéressent qu'à une petite partie de cette valeur.
Piège fréquent
<MonContext.Provider value={{ a, b, setA, setB }}> crée un nouvel objet littéral à chaque rendu du Provider : même si seul a a changé, tous les composants qui lisent useContext(MonContext) se re-rendent, y compris ceux qui ne s'intéressent qu'à b.
| Solution au prop-drilling | Complexité | Cas d'usage typique |
|---|---|---|
| Context API | Faible | Thème, authentification, langue — données peu fréquentes |
| Zustand / Redux | Moyenne à élevée | État global complexe, mises à jour fréquentes et ciblées |
Et ensuite ?
Pour un état global complexe, Context montre vite ses limites : Zustand ou Redux, abordés plus loin, deviennent alors pertinents. Mais avant cela, la prochaine étape consiste à extraire une logique réutilisable avec des hooks personnalisés.
Commandes & code
Context API
Partager une donnée à travers l'arbre de composants sans prop-drilling manuel.
import { createContext, useContext, useState } from "react";
// 1. Créer le contexte, avec une valeur par défaut (utilisée hors Provider)
const ThemeContext = createContext("clair");
// 2. Fournir une valeur avec un Provider, quelque part en haut de l'arbre
function App() {
const [theme, setTheme] = useState("clair");
return (
<ThemeContext.Provider value={theme}>
<Layout />
<button onClick={() => setTheme(theme === "clair" ? "sombre" : "clair")}>
Changer de thème
</button>
</ThemeContext.Provider>
);
}
// 3. Consommer le contexte, à N'IMPORTE QUELLE profondeur, sans props intermédiaires
function Layout() {
return <Sidebar />; // pas besoin de recevoir/transmettre "theme" ici
}
function Sidebar() {
const theme = useContext(ThemeContext); // lit directement la valeur du Provider le plus proche
return <div className={`sidebar sidebar--${theme}`}>...</div>;
}// Pattern robuste : contexte + reducer/state encapsulés dans un Provider custom + hook dédié
const AuthContext = createContext(undefined);
function AuthProvider({ children }) {
const [utilisateur, setUtilisateur] = useState(null);
async function connexion(email, mdp) {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, mdp }),
});
const data = await res.json();
setUtilisateur(data.utilisateur);
}
function deconnexion() {
setUtilisateur(null);
}
const valeur = { utilisateur, connexion, deconnexion };
return <AuthContext.Provider value={valeur}>{children}</AuthContext.Provider>;
}
// Hook custom : masque createContext/useContext, et garde une erreur claire hors Provider
function useAuth() {
const contexte = useContext(AuthContext);
if (contexte === undefined) {
throw new Error("useAuth doit être utilisé à l'intérieur d'un AuthProvider");
}
return contexte;
}
function BoutonConnexion() {
const { utilisateur, deconnexion } = useAuth();
return utilisateur
? <button onClick={deconnexion}>Déconnexion ({utilisateur.nom})</button>
: <a href="/login">Se connecter</a>;
}// Piège de performance : chaque changement de "valeur" re-rend TOUS les consommateurs
function ProviderNonOptimal({ children }) {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
// MAUVAIS : nouvel objet littéral à chaque rendu -> tous les useContext() re-rendent,
// même les composants qui ne lisent QUE "a" alors que seul "b" a changé
return (
<MonContext.Provider value={{ a, b, setA, setB }}>
{children}
</MonContext.Provider>
);
}
// MIEUX : séparer en plusieurs contextes selon la fréquence/l'indépendance de changement
const ValeurAContext = createContext(null);
const ValeurBContext = createContext(null);
// -> un composant qui ne lit que ValeurAContext ignore les changements de BRésumé
useContextlit la valeur duProviderle plus proche dans l'arbre, sans transmission manuelle.- Un hook custom (
useAuth,useTheme) encapsule proprementcreateContext/useContextavec une garde d'erreur. - Chaque changement de la valeur d'un Provider re-rend TOUS ses consommateurs directs.
- Context n'est pas un state manager global optimisé : au-delà d'un certain besoin, Zustand/Redux deviennent pertinents.
Exercices pratiques
Mission : arrêter l'hémorragie de re-rendus du ProviderNonOptimal
Objectif : Diagnostiquer pourquoi tous les consommateurs d'un Context se re-rendent inutilement, et corriger en séparant les contextes.
Contexte
L'application a un seul AppContext.Provider qui expose { a, b, setA, setB }. Le Profiler React montre qu'un composant AfficheA, qui ne lit que a via useContext(AppContext), se re-rend à chaque fois que setB est appelé ailleurs dans l'application, même quand a n'a pas bougé d'un pouce. Un autre composant utilise useContext(AuthContext) en dehors de tout AuthProvider dans un test unitaire, et l'application plante avec un message d'erreur explicite plutôt qu'un bug silencieux.