Passer des props à travers des couches de composants désintéressés est un travail fastidieux. Un jour, vous livrez une fonctionnalité, et le lendemain, vous devez modifier six fichiers juste pour renommer une seule prop. C'est cela, le "prop drilling" en un mot : un parent possède des données, un enfant profond en a besoin, et chaque composant entre les deux devient un coursier. L'application fonctionne toujours, mais la base de code devient fragile. Supprimez une couche intermédiaire, et la moitié de l'arbre s'effondre. Changez un type, et TypeScript se plaint à travers trois répertoires. L'API React Context existe pour éliminer entièrement ces intermédiaires.
À quoi ressemble réellement le prop drilling
Imaginez une structure d'application standard. Vous avez un composant App qui récupère l'utilisateur actuel. À l'intérieur d'App se trouve un Layout, à l'intérieur de Layout se trouve un Sidebar, à l'intérieur de Sidebar est niché un Navigation, et enfin, à l'intérieur de Navigation, vous trouvez le UserAvatar qui a réellement besoin de l'objet utilisateur.
Votre code finit par ressembler à ceci :
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar et Navigation ne font rien de cet objet utilisateur, si ce n'est le transmettre plus bas. Ils accumulent des props qu'ils ne possèdent pas, leurs interfaces gonflent, et les tester nécessite de simuler des données qu'ils ne touchent jamais. Le vrai crime est la rapidité avec laquelle cela se propage. Ajoutez un flag isLoggedIn, une chaîne locale ou une valeur theme, et le même défilé se répète.
Comment l'API Context change la donne
Considérez l'API Context comme un routeur WiFi. Au lieu de tirer de longs câbles dans chaque pièce pour atteindre chaque appareil, le routeur envoie un signal par les airs. Tout appareil à portée peut se connecter directement. En termes de React, le routeur est le Provider, le signal est votre état ou vos données, et l'appareil est n'importe quel composant imbriqué qui appelle useContext.
La configuration comporte trois éléments mobiles :
React.createContext()crée le canal de données.- Le Provider enveloppe une section de votre arbre et transmet une valeur.
- Le hook
useContextpermet aux descendants de recevoir cette valeur sans toucher aux props intermédiaires.
Vous avez toujours un arbre, mais les branches entre la racine et la feuille n'ont plus besoin de s'accorder sur un contrat de coursier.
Construire un Context de zéro
Construisons un exemple concret avec des paramètres de thème, car la plupart des applications ont besoin d'un mode clair ou sombre à un moment donné.
D'abord, créez l'objet context. C'est le tuyau :
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
Ensuite, enveloppez votre application dans le provider. Généralement, cela se passe près de la racine :
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Désormais, n'importe quel descendant peut capter le signal directement. Voici un bouton de bascule (toggle) enfoui profondément dans l'interface :
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Remarquez que Layout, Sidebar et Navigation ne voient jamais la prop theme. Ils s'affichent normalement, et ThemeToggle récupère ce dont il a besoin directement depuis le context. Le câblage est invisible de l'extérieur, ce qui est précisément le but recherché.
Où le Context s'intègre réellement
Le Context fonctionne mieux pour les données que de nombreux composants distants partagent, mais qu'aucun parent unique ne possède proprement. Les bons candidats incluent :
- Les paramètres de thème tels que le mode clair ou sombre, les couleurs d'accentuation ou l'échelle de police.
- L'état d'authentification comme l'objet utilisateur actuel, le statut de connexion ou l'expiration de la session.
- La langue et la locale pour l'internationalisation.
- Les données du panier d'achat qui doivent rester synchronisées entre un badge d'en-tête, un menu déroulant de mini-panier et une page de paiement.
Résistez à la tentation d'injecter chaque morceau d'état local dans le Context. Un champ de formulaire situé deux niveaux plus bas n'a pas besoin d'une diffusion globale. Réservez le Context pour les véritables préoccupations transversales, et laissez le reste sous forme de props ordinaires.
Pièges de performance et comment les éviter
Le Context n'est pas gratuit. Lorsqu'une valeur de contexte est mise à jour, chaque composant connecté à ce contexte subit un re-render, même si la partie de la valeur qui l'intéresse n'a pas changé. L'erreur classique consiste à injecter un nouvel objet littéral dans le Provider à chaque rendu du parent.
Dans notre exemple de thème, chaque fois que le ThemeProvider subit un re-render parce que son propre parent a été mis à jour, l'expression { theme, setTheme } crée un tout nouvel objet. React voit une nouvelle référence, et tous les consommateurs sont mis à jour. Si votre thème change rarement mais que l'état de votre application change souvent, vous payez pour des rendus inutiles.
La solution est double.
Divisez vos contextes selon la fréquence de mise à jour. Un UserContext qui change une fois par connexion ne devrait pas partager un provider avec un NotificationContext qui se met à jour toutes les quelques secondes. Gardez-les séparés pour que les données statiques ne subissent pas le même train de re-renders que les données volatiles.
Enveloppez la valeur dans useMemo lorsque la valeur est un objet ou un tableau. Donnez à React une référence stable :
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Désormais, l'identité de l'objet ne change que lorsque theme change réellement. Les composants descendants qui dépendent du contexte mais qui sont protégés par React.memo plus bas dans l'arborescence ignoreront le travail inutile.
Les erreurs qui vous font perdre du temps
Les deux erreurs qui apparaissent encore dans le code de production sont faciles à éviter.
Premièrement, oublier d'exporter le contexte lui-même. Si vous n'exportez que le wrapper Provider et gardez l'objet contexte privé, un développeur écrivant une nouvelle fonctionnalité ne pourra pas appeler useContext sans refactoriser votre module. Exportez le contexte afin que les consommateurs puissent importer proprement à la fois le provider et le hook de consommation.
Deuxièmement, appeler useContext en dehors du Provider correspondant. Si ThemeToggle est rendu dans une branche de l'arbre qui n'est pas enveloppée par ThemeProvider, le hook renvoie la valeur par défaut passée à createContext, ou undefined si vous n'en avez pas passé. Cela conduit à des échecs silencieux comme cannot read property of undefined. Vous pouvez vous en prémunir en assignant une valeur par défaut cohérente ou en lançant une erreur explicite dès l'appel du hook.
Context vs Redux : Restez simple
Vous n'avez pas toujours besoin de Redux. Pour les projets de taille moyenne, le Context couplé à useState ou useReducer couvre l'essentiel du partage d'état. Redux brille lorsque vous avez besoin de "time-travel debugging", de middlewares complexes ou de transactions globales qui doivent être annulées en séquence. Si toute votre gestion d'état se résume à un objet utilisateur, une chaîne de caractères pour le thème et un tableau pour le panier, une bibliothèque de store n'ajoutera que du boilerplate que vous n'exploiterez jamais.
Cela dit, le Context n'est pas un système complet de gestion d'état en soi. Il ne vous donne pas un snapshot global unique et il ne regroupe pas les mises à jour entre des contextes non liés. Utilisez-le comme un remplacement au "prop drilling", et non comme un système d'exploitation pour toute votre couche de données.
Ce qu'il faut retenir
Arrêtez de faire passer des props à travers des composants qui n'en ont pas besoin. Créez des contextes ciblés pour les données qui traversent réellement votre arbre, enveloppez les providers suffisamment haut pour couvrir les consommateurs, et stabilisez toujours l'objet de valeur lorsque vous passez des collections ou des fonctions. L'API Context rend votre code React direct : les props restent locales, les données globales circulent sans fil, et les limites de vos composants restent propres.
