React context est la façon officielle de partager des données dans un arbre de composants sans les transmettre à la main sous forme de props à chaque niveau. Si vous avez déjà ajouté une prop inutile à chaque composant d'une chaîne juste pour livrer le thème, l'utilisateur connecté ou la langue choisie à un composant situé cinq niveaux plus bas, vous avez rencontré le problème du « prop drilling ». Dans cet article, je montre comment mettre en place l'API Context correctement, l'envelopper dans un hook personnalisé, éviter les pièges de performance les plus courants et reconnaître quand il ne faut pas recourir au context — le tout avec du code concret.
Qu'est-ce que le prop drilling, concrètement ?
Supposons que les infos de session vivent tout en haut et que seul un composant Avatar profondément imbriqué les utilise. Les composants Layout, Sidebar et UserMenu intermédiaires ne lisent jamais la valeur, mais sont obligés d'accepter une prop juste pour la transmettre plus bas :
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} />;
}
// Layout -> Sidebar -> UserMenu -> Avatar
// chacun transmet « user » uniquement au suivant
Le problème : la signature des composants intermédiaires est polluée par des données dont ils ne dépendent pas, le refactoring devient plus dur, et renommer une seule prop se répercute sur toute la chaîne. Le context court-circuite cette chaîne : vous fournissez la valeur une fois en haut et vous la lisez directement en bas.
Le plus petit exemple fonctionnel
Le context comporte trois parties : créer le context avec createContext, diffuser une valeur via un Provider, et lire cette valeur avec useContext.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Enveloppez ensuite la racine de votre application dans le provider et appelez le hook depuis n'importe quel descendant :
function ThemeButton() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Thème : {theme}</button>;
}
function App() {
return (
<ThemeProvider>
<ThemeButton />
</ThemeProvider>
);
}
Désormais, quel que soit le nombre de couches entre ThemeButton et App, aucune n'a à transporter une prop theme.
Un accès sûr grâce à un hook personnalisé
Ci-dessus, j'ai écrit un petit hook personnalisé nommé useTheme ; privilégiez toujours ce modèle. Il a deux avantages : il masque les détails internes du context à l'appelant et peut lever une erreur explicite s'il est utilisé sans provider.
export function useTheme() {
const ctx = useContext(ThemeContext);
if (ctx === undefined) {
throw new Error("useTheme doit être utilisé dans un <ThemeProvider>");
}
return ctx;
}
Si vous définissez la valeur initiale avec createContext(undefined), oublier d'ajouter le provider vous donne une erreur claire au lieu de renvoyer silencieusement une mauvaise valeur par défaut. En équipe, cela accélère nettement le débogage.
Le plus grand piège de performance : les rendus inutiles
Ce qui est le plus mal compris à propos du context : quand la value du provider change, tous les composants qui consomment ce context se re-rendent. Les ennuis commencent quand vous créez une nouvelle référence d'objet à chaque rendu :
// MAUVAIS : un nouvel objet { } à chaque rendu -> tous les consommateurs se re-rendent
<ThemeContext.Provider value={{ theme, toggle }}>
Même si le composant provider se re-rend pour une raison sans rapport, cette ligne produit un nouvel objet, et comme React compare les références avec Object.is, il considère que c'est « modifié ». La solution est de stabiliser la valeur avec useMemo :
const value = useMemo(() => ({ theme, toggle }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
Une seconde technique consiste à séparer le context : si vous placez les données qui changent souvent (ex. une saisie de texte en direct) et les fonctions qui changent rarement (ex. dispatch) dans deux contexts distincts, la valeur rapide n'affecte que les composants qui la lisent. Règle générale : plus un même context transporte de choses indépendantes, plus le risque de rendus inutiles augmente.
L'associer à useReducer
À mesure que la logique d'état grandit, utiliser useReducer plutôt que useState et l'associer au context est un modèle propre. Cela se comporte comme un petit « store » global mais ne nécessite aucune bibliothèque externe :
const CartContext = createContext(null);
function cartReducer(state, action) {
switch (action.type) {
case "add": return [...state, action.item];
case "remove": return state.filter(i => i.id !== action.id);
default: return state;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const value = useMemo(() => ({ items, dispatch }), [items]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
React garde la fonction dispatch stable, vous pouvez donc l'utiliser en toute sécurité dans les tableaux de dépendances de memo.
Quand ne pas utiliser le context
Le context n'est pas la réponse à tous les problèmes. Dans les situations suivantes, regardez ailleurs :
- Mises à jour fréquentes et à large impact (ex. des milliers d'abonnés à chaque frappe) : le context seul n'est pas un gestionnaire d'état optimisé pour la performance ; des outils comme Zustand, Jotai ou Redux Toolkit sont plus efficaces dans ce scénario.
- Données serveur (provenant d'une API, nécessitant cache et nouvelles tentatives) : une couche de données comme TanStack Query est bien plus adaptée que le context.
- Une à deux profondeurs seulement : si le prop drilling ne fait que deux couches, transmettre une prop est souvent plus lisible que de mettre en place un context.
Le point fort du context, ce sont les données qui changent rarement mais sont lues à de nombreux endroits : thème, état d'authentification, langue/localisation, feature flags.
Questions fréquentes
Le context remplace-t-il Redux ?
En partie. Le context est un mécanisme de « transport de données », pas une bibliothèque complète de gestion d'état. Pour un état global simple, useReducer + context suffit ; mais dès que vous avez besoin de middleware, de devtools ou d'une optimisation des rendus par sélecteurs, Redux Toolkit ou Zustand prend tout son sens.
Imbriquer plusieurs contexts pose-t-il problème ?
Non, c'est un modèle courant et sain. Utiliser des providers distincts pour le thème, l'utilisateur et le panier est généralement préférable, pour la performance comme pour la lisibilité, à un seul context géant. Vous pouvez garder la racine propre en regroupant les providers imbriqués dans un seul composant AppProviders.
useMemo est-il vraiment nécessaire ?
Si la valeur du provider contient un objet ou une fonction et que le provider peut se re-rendre, oui. Si la valeur est un type primitif (string, number, boolean), React compare la valeur plutôt qu'une référence, et useMemo n'est pas nécessaire.
Vous voulez simplifier le flux d'état de votre architecture React ? Je peux vous aider à bâtir une structure de context performante et sans prop drilling — contactez-moi.