React Context API : gestion état global

Front-end 24/03/2026 20:00:00 angularforall.com
React Context Api Usecontext État Global Prop Drilling
React Context API : gestion état global

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é.

Solution : Le Context API permet de rendre une valeur disponible à n'importe quel composant de l'arborescence, sans passer par les props intermédiaires. C'est le mécanisme natif de React pour partager un état global.
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>
);
Organisation des fichiers : Créez un dossier 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>;
}
Pattern recommandé : Exportez toujours un Hook personnalisé (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
Règle pratique : Commencez toujours par le Context API. Si vous rencontrez des problèmes de performance mesurables ou une complexité qui dépasse vos besoins, migrez vers Zustand ou Redux. Ne sur-architecturez pas dès le départ.

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
Conclusion : Le Context API est la solution native de React pour éviter le prop drilling et partager un état global. Avec le pattern createContext + Provider + Hook personnalisé, enrichi de 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.

Partager