Maîtrisez le Context API React pour partager un état global sans prop drilling. Exemples concrets : thème, auth, panier avec useMemo et useReducer.
Le problème : le prop drilling
Dans React, les données circulent du parent vers les enfants via les props. C'est clair et prévisible pour des arborescences simples. Mais quand votre application grandit, cette approche génère un problème courant : le prop drilling (ou "forage de props").
Le prop drilling se produit quand une donnée doit traverser plusieurs composants intermédiaires qui n'en ont pas besoin, juste pour atteindre un composant profondément imbriqué.
// Exemple de prop drilling : l'utilisateur doit traverser 3 niveaux
function App() {
const [utilisateur, setUtilisateur] = useState({ nom: 'Alice', role: 'admin' });
// App passe utilisateur à Layout, qui ne l'utilise pas
return <Layout utilisateur={utilisateur} />;
}
function Layout({ utilisateur }) {
// Layout passe utilisateur à Sidebar, qui ne l'utilise pas non plus
return (
<div>
<Sidebar utilisateur={utilisateur} />
<MainContent />
</div>
);
}
function Sidebar({ utilisateur }) {
// Sidebar passe utilisateur à ProfilCard, qui en a vraiment besoin
return <ProfilCard utilisateur={utilisateur} />;
}
function ProfilCard({ utilisateur }) {
// Seul ce composant utilise réellement la prop
return <p>Bonjour, {utilisateur.nom} ({utilisateur.role})</p>;
}
Dans cet exemple, Layout et Sidebar reçoivent utilisateur sans l'utiliser. Ce n'est pas grave sur 3 niveaux, mais imaginez 7 ou 8 niveaux avec 4 props différentes… Le code devient difficile à maintenir, et chaque refactoring devient risqué.
| Approche | Avantages | Inconvénients |
|---|---|---|
| Props classiques | Explicite, traçable, simple | Prop drilling sur 3+ niveaux |
| Context API | Accessible partout, pas de prop drilling | Re-rendus si mal optimisé |
| Zustand / Redux | Sélecteurs fins, DevTools puissants | Dépendance externe, courbe d'apprentissage |
createContext et Provider : les bases
Le Context API repose sur trois éléments : createContext pour créer le contexte, un composant Provider pour fournir la valeur, et useContext pour la consommer. Voici comment les assembler.
Étape 1 — Créer le contexte
import { createContext } from 'react';
// createContext reçoit la valeur par défaut du contexte
// Cette valeur est utilisée uniquement si un composant consomme le contexte
// SANS être enveloppé dans un Provider — utile pour les tests
const ThemeContext = createContext({
theme: 'light', // valeur par défaut
toggleTheme: () => {}, // fonction vide par défaut (évite les erreurs)
});
export default ThemeContext;
Étape 2 — Créer le Provider
import { createContext, useState } from 'react';
// Bonne pratique : créer le contexte et le Provider dans le même fichier
export const ThemeContext = createContext(null);
// Le Provider est un composant classique qui enveloppe l'arborescence
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// La fonction de bascule
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// La valeur fournie à tous les consumers
const valeur = { theme, toggleTheme };
return (
// Toute l'arborescence dans children aura accès à cette valeur
<ThemeContext.Provider value={valeur}>
{children}
</ThemeContext.Provider>
);
}
Étape 3 — Brancher le Provider à la racine
// index.jsx ou main.jsx — point d'entrée de l'application
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from './contexts/ThemeContext';
import App from './App';
createRoot(document.getElementById('root')).render(
<StrictMode>
{/* ThemeProvider enveloppe toute l'app → contexte disponible partout */}
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);
src/contexts/ avec un fichier par contexte (ex: ThemeContext.jsx, AuthContext.jsx). Chaque fichier exporte le contexte ET son Provider. Cette convention est adoptée par la quasi-totalité des projets React en production.
useContext : consommer le contexte
useContext est le Hook qui permet à un composant de lire la valeur du contexte le plus proche dans l'arborescence. Il remplace l'ancienne API Context.Consumer par une syntaxe beaucoup plus simple.
Consommer un contexte dans un composant
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function BoutonTheme() {
// useContext retourne directement la valeur fournie par le Provider
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#ffffff' : '#1a1a2e',
color: theme === 'light' ? '#1a1a2e' : '#ffffff',
border: '1px solid currentColor',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{theme === 'light' ? '🌙 Mode sombre' : '☀️ Mode clair'}
</button>
);
}
// Ce composant peut être n'importe où dans l'arborescence
// Il n'a pas besoin que ses parents lui passent theme en prop
function HeaderApp() {
return (
<header>
<h1>Mon Application</h1>
<BoutonTheme /> {/* Accède au thème directement */}
</header>
);
}
Créer un Hook personnalisé pour encapsuler useContext
Bonne pratique essentielle : créer un Hook useTheme() qui encapsule useContext. Cela évite d'importer le contexte partout, et permet d'ajouter une vérification d'usage correct.
// Dans ThemeContext.jsx — exporter le hook avec le contexte
export function useTheme() {
const contexte = useContext(ThemeContext);
// Vérification : si le hook est utilisé en dehors du Provider, erreur claire
if (contexte === null) {
throw new Error(
'useTheme doit être utilisé à l\'intérieur d\'un ThemeProvider. ' +
'Vérifiez que ThemeProvider enveloppe votre composant dans l\'arborescence.'
);
}
return contexte;
}
// Dans n'importe quel composant — import simplifié
import { useTheme } from '../contexts/ThemeContext';
function MonComposant() {
// Plus besoin d'importer ThemeContext séparément
const { theme, toggleTheme } = useTheme();
return <p>Thème actuel : {theme}</p>;
}
useTheme, useAuth, usePanier) plutôt que d'exposer directement le contexte. Ce pattern est adopté par React Query, React Router et toutes les grandes bibliothèques React.
Optimiser le Context avec useMemo
Le problème de performance le plus courant avec le Context : tous les consumers se re-rendent quand la valeur du Provider change. Si vous passez un objet non mémorisé comme valeur, cet objet est recréé à chaque rendu du Provider — ce qui déclenche un re-rendu de TOUS les consumers, même si les données n'ont pas vraiment changé.
// ❌ NON OPTIMISÉ : valeur recréée à chaque rendu
export function ThemeProviderBogue({ children }) {
const [theme, setTheme] = useState('light');
const [fontSize, setFontSize] = useState(16);
// Cet objet est NOUVEAU à chaque rendu de ThemeProviderBogue
// → Tous les useContext(ThemeContext) se re-rendent inutilement
return (
<ThemeContext.Provider value={{ theme, setTheme, fontSize, setFontSize }}>
{children}
</ThemeContext.Provider>
);
}
// ✅ OPTIMISÉ : valeur mémorisée avec useMemo
import { useState, useMemo, useCallback } from 'react';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const [fontSize, setFontSize] = useState(16);
// Mémoriser les setters qui dépendent d'une logique
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []); // Pas de dépendances : setTheme est stable
const augmenterPolice = useCallback(() => {
setFontSize(prev => Math.min(prev + 2, 24)); // Max 24px
}, []);
const reinitialiserPolice = useCallback(() => {
setFontSize(16);
}, []);
// useMemo garantit que l'objet ne change que si theme ou fontSize change
const valeur = useMemo(() => ({
theme,
fontSize,
toggleTheme,
augmenterPolice,
reinitialiserPolice,
estModeSombre: theme === 'dark',
}), [theme, fontSize, toggleTheme, augmenterPolice, reinitialiserPolice]);
return (
<ThemeContext.Provider value={valeur}>
{children}
</ThemeContext.Provider>
);
}
Séparer les contextes lecture/écriture
Technique avancée : créer deux contextes séparés pour la lecture et l'écriture. Les composants qui ne font que lire ne se re-rendent pas quand les fonctions de mise à jour changent (ce qui n'arrive jamais avec useCallback), et vice-versa.
// Deux contextes distincts pour optimiser les re-rendus
const ThemeValeurContext = createContext(null); // Pour lire le thème
const ThemeActionsContext = createContext(null); // Pour modifier le thème
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Les actions sont stables — jamais recréées
const actions = useMemo(() => ({
setTheme,
toggleTheme: () => setTheme(p => p === 'light' ? 'dark' : 'light'),
}), []); // setTheme est garantie stable par React
// La valeur change quand theme change
const valeur = useMemo(() => ({ theme, estSombre: theme === 'dark' }), [theme]);
return (
<ThemeActionsContext.Provider value={actions}>
<ThemeValeurContext.Provider value={valeur}>
{children}
</ThemeValeurContext.Provider>
</ThemeActionsContext.Provider>
);
}
// Un composant qui modifie le thème mais ne l'affiche pas
// → Ne se re-rendra PAS quand theme change (il lit seulement les actions)
function BoutonToggle() {
const { toggleTheme } = useContext(ThemeActionsContext);
return <button onClick={toggleTheme}>Basculer</button>;
}
// Un composant qui affiche le thème mais ne le modifie pas
// → Se re-rend seulement quand theme change
function AffichageTheme() {
const { theme } = useContext(ThemeValeurContext);
return <p>Mode : {theme}</p>;
}
Context + useReducer : pattern avancé
Combiner Context et useReducer est le pattern le plus puissant pour gérer un état global complexe sans bibliothèque externe. C'est l'approche recommandée avant d'introduire Redux ou Zustand dans un projet.
import { createContext, useContext, useReducer, useMemo } from 'react';
// --- Définition de l'état et du reducer ---
const etatInitial = {
utilisateur: null,
estConnecte: false,
chargement: false,
erreurAuth: null,
};
function authReducer(etat, action) {
switch (action.type) {
case 'CONNEXION_DEBUT':
return { ...etat, chargement: true, erreurAuth: null };
case 'CONNEXION_SUCCES':
return {
...etat,
chargement: false,
estConnecte: true,
utilisateur: action.payload,
erreurAuth: null,
};
case 'CONNEXION_ECHEC':
return {
...etat,
chargement: false,
estConnecte: false,
erreurAuth: action.payload,
};
case 'DECONNEXION':
return etatInitial; // Retour à l'état initial
case 'MAJ_PROFIL':
return {
...etat,
utilisateur: { ...etat.utilisateur, ...action.payload },
};
default:
return etat;
}
}
// --- Création du contexte ---
const AuthContext = createContext(null);
// --- Provider avec useReducer ---
export function AuthProvider({ children }) {
const [etat, dispatch] = useReducer(authReducer, etatInitial);
// Mémoriser les actions pour éviter les re-rendus inutiles
const actions = useMemo(() => ({
// Action de connexion — appel API intégré dans l'action
connecter: async (email, motDePasse) => {
dispatch({ type: 'CONNEXION_DEBUT' });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, motDePasse }),
});
if (!response.ok) {
throw new Error('Identifiants incorrects');
}
const utilisateur = await response.json();
// Stocker le token en mémoire (pas localStorage pour la sécurité)
dispatch({ type: 'CONNEXION_SUCCES', payload: utilisateur });
} catch (err) {
dispatch({ type: 'CONNEXION_ECHEC', payload: err.message });
}
},
deconnecter: () => {
// Nettoyer la session côté serveur si besoin
fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
dispatch({ type: 'DECONNEXION' });
},
mettreAJourProfil: (donnees) => {
dispatch({ type: 'MAJ_PROFIL', payload: donnees });
},
}), []); // Actions stables — dispatch est garanti stable par React
// Combiner état et actions dans la valeur du contexte
const valeur = useMemo(() => ({
...etat, // utilisateur, estConnecte, chargement, erreurAuth
...actions, // connecter, deconnecter, mettreAJourProfil
}), [etat, actions]);
return (
<AuthContext.Provider value={valeur}>
{children}
</AuthContext.Provider>
);
}
// --- Hook d'accès ---
export function useAuth() {
const contexte = useContext(AuthContext);
if (!contexte) throw new Error('useAuth doit être dans AuthProvider');
return contexte;
}
Utilisation dans les composants :
// Formulaire de connexion
function FormulaireConnexion() {
const { connecter, chargement, erreurAuth } = useAuth();
const [email, setEmail] = useState('');
const [motDePasse, setMdp] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
await connecter(email, motDePasse);
// Pas besoin de gérer la navigation ici — le contexte met à jour estConnecte
};
return (
<form onSubmit={handleSubmit}>
{erreurAuth && <p style={{ color: 'red' }}>{erreurAuth}</p>}
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
<input value={motDePasse} onChange={e => setMdp(e.target.value)} type="password" placeholder="Mot de passe" />
<button type="submit" disabled={chargement}>
{chargement ? 'Connexion...' : 'Se connecter'}
</button>
</form>
);
}
// Menu utilisateur — accède à l'état depuis n'importe où dans l'arbre
function MenuUtilisateur() {
const { utilisateur, estConnecte, deconnecter } = useAuth();
if (!estConnecte) return <a href="/login">Se connecter</a>;
return (
<div>
<span>Bonjour, {utilisateur.nom}</span>
<button onClick={deconnecter}>Se déconnecter</button>
</div>
);
}
Cas réels : thème, auth, panier
Voici trois implémentations complètes et prêtes à l'emploi pour les contextes les plus fréquents dans les applications React professionnelles.
Contexte Panier e-commerce
// contexts/PanierContext.jsx
import { createContext, useContext, useReducer, useMemo } from 'react';
const PanierContext = createContext(null);
function panierReducer(etat, action) {
switch (action.type) {
case 'AJOUTER': {
const existant = etat.items.find(i => i.id === action.payload.id);
if (existant) {
// Incrémenter la quantité si le produit est déjà dans le panier
return {
...etat,
items: etat.items.map(i =>
i.id === action.payload.id
? { ...i, quantite: i.quantite + 1 }
: i
),
};
}
// Ajouter avec quantité = 1
return { ...etat, items: [...etat.items, { ...action.payload, quantite: 1 }] };
}
case 'RETIRER':
return { ...etat, items: etat.items.filter(i => i.id !== action.payload) };
case 'MODIFIER_QTE':
return {
...etat,
items: etat.items.map(i =>
i.id === action.payload.id
? { ...i, quantite: Math.max(1, action.payload.quantite) }
: i
),
};
case 'VIDER':
return { ...etat, items: [] };
default:
return etat;
}
}
export function PanierProvider({ children }) {
const [etat, dispatch] = useReducer(panierReducer, { items: [] });
// Calculs dérivés mémorisés
const totalItems = useMemo(
() => etat.items.reduce((acc, i) => acc + i.quantite, 0),
[etat.items]
);
const totalPrix = useMemo(
() => etat.items.reduce((acc, i) => acc + i.prix * i.quantite, 0),
[etat.items]
);
const valeur = useMemo(() => ({
items: etat.items,
totalItems,
totalPrix,
ajouter: (produit) => dispatch({ type: 'AJOUTER', payload: produit }),
retirer: (id) => dispatch({ type: 'RETIRER', payload: id }),
modifierQte: (id, qte) => dispatch({ type: 'MODIFIER_QTE', payload: { id, quantite: qte } }),
vider: () => dispatch({ type: 'VIDER' }),
}), [etat.items, totalItems, totalPrix]);
return <PanierContext.Provider value={valeur}>{children}</PanierContext.Provider>;
}
export const usePanier = () => {
const ctx = useContext(PanierContext);
if (!ctx) throw new Error('usePanier doit être dans PanierProvider');
return ctx;
};
Utilisation depuis n'importe quel composant :
// Icône panier dans le header
function IconePanier() {
const { totalItems } = usePanier();
return (
<button>
🛒 {totalItems > 0 && <span className="badge">{totalItems}</span>}
</button>
);
}
// Bouton "Ajouter au panier" sur une fiche produit
function BoutonAjouterPanier({ produit }) {
const { ajouter } = usePanier();
return (
<button onClick={() => ajouter(produit)}>
Ajouter au panier
</button>
);
}
// Page récapitulatif du panier
function PagePanier() {
const { items, totalPrix, retirer, modifierQte, vider } = usePanier();
if (items.length === 0) return <p>Votre panier est vide.</p>;
return (
<div>
{items.map(item => (
<div key={item.id}>
<span>{item.nom}</span>
<input
type="number"
value={item.quantite}
onChange={e => modifierQte(item.id, Number(e.target.value))}
min="1"
/>
<span>{(item.prix * item.quantite).toFixed(2)}€</span>
<button onClick={() => retirer(item.id)}>Supprimer</button>
</div>
))}
<p><strong>Total : {totalPrix.toFixed(2)}€</strong></p>
<button onClick={vider}>Vider le panier</button>
</div>
);
}
Séparer les contextes par domaine
Une erreur courante est de mettre tout l'état global dans un seul contexte "AppContext". Cette approche provoque des re-rendus massifs inutiles et rend le code difficile à maintenir. La bonne pratique est de créer un contexte par domaine fonctionnel.
// ❌ Anti-pattern : tout dans un seul contexte
const AppContext = createContext(null);
function AppProvider({ children }) {
const [utilisateur, setUtilisateur] = useState(null);
const [theme, setTheme] = useState('light');
const [panier, setPanier] = useState([]);
const [notifications, setNotifs] = useState([]);
const [langue, setLangue] = useState('fr');
// Chaque changement de n'importe quelle valeur re-rend TOUS les consumers !
const valeur = { utilisateur, setUtilisateur, theme, setTheme, panier, setPanier, ... };
return <AppContext.Provider value={valeur}>{children}</AppContext.Provider>;
}
// ✅ Bonne pratique : un contexte par domaine
// Structure recommandée :
// src/contexts/
// ├── AuthContext.jsx → Authentification, utilisateur courant
// ├── ThemeContext.jsx → Thème clair/sombre, police, couleurs
// ├── PanierContext.jsx → Items, total, ajout/suppression
// ├── I18nContext.jsx → Langue, traductions
// └── NotifContext.jsx → Notifications toast, alertes
// Composer les Providers dans App.jsx ou index.jsx
function Providers({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<PanierProvider>
<NotifProvider>
{children}
</NotifProvider>
</PanierProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Ou avec un helper pour réduire l'imbrication
function combinerProviders(providers) {
return providers.reduce(
(Acc, Provider) =>
({ children }) => <Provider><Acc>{children}</Acc></Provider>
);
}
const AppProviders = combinerProviders([AuthProvider, ThemeProvider, PanierProvider]);
// Utilisation propre sans pyramide
function App() {
return (
<AppProviders>
<RouterRoot />
</AppProviders>
);
}
Limites du Context et alternatives
Le Context API est puissant mais n'est pas adapté à tous les scénarios. Comprendre ses limites vous aidera à choisir le bon outil.
Quand le Context montre ses limites
// Problème 1 : mises à jour très fréquentes
// Le Context n'est pas optimisé pour des données qui changent à chaque frame
// Exemple : position de la souris, valeur d'un slider en temps réel
function MouseTrackerBogue() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
// mousemove déclenche des centaines d'événements/seconde
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
// Mettre cette valeur dans un Context re-rendrait TOUS les consumers
// des centaines de fois par seconde — catastrophique !
// → Pour ce cas : passer par des refs, ou Zustand avec des sélecteurs fins
}
| Besoin | Solution recommandée |
|---|---|
| État partagé simple (thème, langue, auth) | Context API + useMemo |
| État global complexe avec actions multiples | Context + useReducer |
| État mis à jour très fréquemment | Zustand (sélecteurs fins) |
| Cache serveur / données asynchrones | TanStack Query |
| État complexe avec historique / time travel | Redux Toolkit |
| Formulaires complexes avec validation | React Hook Form |
Checklist et bonnes pratiques
- Un contexte = un domaine fonctionnel (auth, thème, panier…)
- La valeur du Provider est mémorisée avec
useMemo - Les fonctions dans la valeur sont mémorisées avec
useCallback - Un Hook personnalisé (
useTheme,useAuth) est exporté à la place du contexte brut - Le Hook vérifie qu'il est utilisé dans un Provider (
if (!ctx) throw Error) - Le Provider et le contexte sont dans le même fichier (
contexts/XxxContext.jsx) - Je n'utilise pas le Context pour des données qui changent >10 fois/seconde
- J'ai une valeur par défaut dans
createContext()pour faciliter les tests - Les Providers sont composés proprement à la racine de l'application
- J'ai mesuré les re-rendus avec React DevTools avant et après l'optimisation
useMemo pour l'optimisation et useReducer pour la logique complexe, vous couvrez 80% des besoins en state management sans dépendance externe. Maîtriser ces patterns vous rend immédiatement opérationnel sur n'importe quel projet React professionnel.