Retour au cours

frontend / react

Context API

Leçon 101 exercice

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 Provider et la lire avec useContext
  • 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-drillingComplexitéCas d'usage typique
Context APIFaibleThème, authentification, langue — données peu fréquentes
Zustand / ReduxMoyenne à é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.

jsx
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>;
}
jsx
// 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>;
}
jsx
// 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 B

Résumé

  • useContext lit la valeur du Provider le plus proche dans l'arbre, sans transmission manuelle.
  • Un hook custom (useAuth, useTheme) encapsule proprement createContext/useContext avec 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

1 disponible
1

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.

Résoudre l’exercice →