Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1116 minIntermédiaire

Partager l'état : remonter l'état, Context et hooks personnalisés

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • remonter un état dans le parent commun de deux composants ;
  • reconnaître le prop drilling et le résoudre avec un Context ;
  • écrire un hook personnalisé qui réutilise une logique avec état ;
  • choisir le bon outil pour chaque type d'état.

4 exercices corrigés

Quand plusieurs composants ont besoin de la même donnée, on la place dans leur ancêtre commun et on la transmet par les props : c'est « remonter l'état ». Quand la donnée est utile presque partout (l'utilisateur connecté), on la rend accessible à tout l'arbre avec un Context, sans la faire passer de composant en composant. Et quand une même logique se répète, on l'extrait dans un hook personnalisé. Ce chapitre présente ces trois outils et dit quand utiliser chacun.

Remonter l'état

Deux composants frères doivent rester synchronisés : un champ règle le plafond de remboursement, un résumé compte les frais qui le dépassent. Si chacun garde son propre état, ils ne se parlent pas. La solution : déplacer l'état dans leur parent commun, qui le transmet à l'un pour l'afficher et à l'autre pour le modifier.

tsx
import { useState } from 'react';

export function PanneauFrais() {
  const [plafond, setPlafond] = useState(25);   // l'état vit dans le parent commun
  return (
    <>
      <ReglagePlafond plafond={plafond} onChange={setPlafond} />
      <ResumePlafond plafond={plafond} montants={[23.5, 31, 12, 48.9]} />
    </>
  );
}

function ReglagePlafond({ plafond, onChange }: { plafond: number; onChange: (p: number) => void }) {
  return (
    <p>
      <label htmlFor="plafond">Plafond (€)</label>
      <input id="plafond" type="number" value={plafond} onChange={(e) => onChange(Number(e.target.value))} />
    </p>
  );
}

function ResumePlafond({ plafond, montants }: { plafond: number; montants: number[] }) {
  const auDela = montants.filter((m) => m > plafond).length;
  return <p>{auDela} frais au-delà du plafond</p>;
}

Au départ, le résumé affiche « 2 frais au-delà du plafond » (31 et 48,90 € dépassent 25 €). Si l'utilisateur tape 40, il affiche « 1 frais au-delà du plafond ». Les deux enfants sont devenus contrôlés par le parent : ils n'ont plus d'état, seulement des props. C'est exactement ce que fait PageFiches avec FiltreEtat et la liste (chapitre 3).

La méthode, en trois questions :

  1. Quels composants utilisent cette donnée ?
  2. Quel est leur plus proche ancêtre commun ?
  3. L'état va dans cet ancêtre ; il descend par les props, les modifications remontent par des fonctions.

Le problème du prop drilling

L'ancêtre commun est parfois très loin. Le nom de l'utilisateur connecté sert dans l'en-tête, son rôle dans les boutons d'une fiche, son identifiant dans un formulaire. Tout passer par les props donne ceci :

tsx
<App utilisateur={utilisateur}>
  <Layout utilisateur={utilisateur}>              {/* ne s'en sert pas, la transmet */}
    <PageFiche utilisateur={utilisateur}>         {/* ne s'en sert pas, la transmet */}
      <BarreActions utilisateur={utilisateur} />  {/* enfin, celui qui s'en sert */}

C'est le prop drilling (« forage de props ») : des composants intermédiaires reçoivent une prop uniquement pour la passer plus bas. Ajouter un niveau ou renommer la prop oblige à modifier toute la chaîne. Au-delà de deux ou trois niveaux, c'est le signe qu'il faut un Context.

Le Context : une valeur accessible à tout un sous-arbre

Un Context permet à un composant de fournir une valeur, et à n'importe quel descendant de la lire directement, quelle que soit la profondeur. Trois étapes.

1. Créer le contexte, avec sa valeur par défaut (utilisée par un composant qui n'a pas de fournisseur au-dessus de lui) :

tsx
import { createContext } from 'react';

type NotificationsValeur = {
  notifier: (message: string) => void;
};

const NotificationsContext = createContext<NotificationsValeur | null>(null);

2. Fournir la valeur, en enveloppant une partie de l'arbre. Depuis React 19, le contexte s'utilise directement comme composant fournisseur (<NotificationsContext value={…}>) ; l'ancienne écriture <NotificationsContext.Provider value={…}> fonctionne encore mais sera dépréciée.

tsx
import { useState, type ReactNode } from 'react';

type Notification = { id: number; message: string };

export function NotificationsProvider({ children }: { children: ReactNode }) {
  const [notifications, setNotifications] = useState<Notification[]>([]);

  function notifier(message: string) {
    const id = Date.now() + Math.random();
    setNotifications((liste) => [...liste, { id, message }]);
    setTimeout(() => {
      setNotifications((liste) => liste.filter((n) => n.id !== id));   // disparaît après 5 s
    }, 5000);
  }

  return (
    <NotificationsContext value={{ notifier }}>
      {children}
      <div role="status" className="notifications">
        {notifications.map((n) => <p key={n.id}>{n.message}</p>)}
      </div>
    </NotificationsContext>
  );
}

3. Lire la valeur dans un descendant, avec useContext(NotificationsContext) ou, en React 19, use(NotificationsContext). On l'enveloppe presque toujours dans un hook qui vérifie la présence du fournisseur :

tsx
import { use } from 'react';

export function useNotifications() {
  const valeur = use(NotificationsContext);
  if (valeur === null) {
    throw new Error('useNotifications doit être appelé sous <NotificationsProvider>.');
  }
  return valeur;
}

N'importe quel composant, à n'importe quelle profondeur, peut alors notifier l'utilisateur :

tsx
export function BoutonCopier({ texte }: { texte: string }) {
  const { notifier } = useNotifications();
  return <button type="button" onClick={() => notifier(`« ${texte} » copié`)}>Copier</button>;
}

Il suffit de placer <NotificationsProvider> autour de l'application, dans main.tsx. Le role="status" de la zone de notifications fait annoncer chaque message par les lecteurs d'écran (chapitre 14).

useContext(Contexte)use(Contexte) (React 19)
Lit la valeur du fournisseur le plus procheOuiOui
Peut s'appeler dans une condition ou après un returnNon (règles des hooks)Oui
Lit aussi une promesseNonOui (avec Suspense, chapitre 9)

Les deux conviennent ; ce cours utilise use pour les contextes, mais tu liras useContext dans la plupart des projets.

Quel outil pour quel état ?

SituationOutil
Une donnée sert à un seul composantuseState dans ce composant
Deux composants proches doivent la partagerRemonter l'état dans le parent commun, transmettre par les props
Elle sert dans des composants éloignés, et change rarementUn Context
Elle vient de l'APITanStack Query (chapitre 9)
Elle décrit ce que l'utilisateur regarde (filtre, page)L'URL (chapitre 10)
Un état d'interface riche, partagé par beaucoup d'écransUne bibliothèque comme Zustand ou Redux Toolkit

Les hooks personnalisés

Un hook personnalisé est une fonction dont le nom commence par use et qui appelle d'autres hooks. Il sert à réutiliser une logique avec état entre plusieurs composants, comme une fonction sert à réutiliser un calcul. Tu en as déjà écrit : useFiches et useSupprimerFrais au chapitre 9, useNotifications ci-dessus.

Les règles :

  • le nom commence par use suivi d'une majuscule : c'est ce qui permet au linter de vérifier les règles des hooks à l'intérieur ;
  • il peut appeler tous les hooks (useState, useEffect, d'autres hooks personnalisés), en respectant les règles des hooks ;
  • il partage une logique, pas un état : deux composants qui appellent le même hook ont chacun leur propre état (sauf si le hook lit un Context ou un cache partagé, comme useFiches).

Une fonction qui n'appelle aucun hook n'est pas un hook : nomme-la normalement (formatMontant, pas useFormatMontant).

Un hook pour le titre de la page

Chaque page d'AppliFrais doit changer le titre de l'onglet : c'est utile à tous, et indispensable aux utilisateurs de lecteur d'écran, qui entendent le titre à chaque changement de page (chapitre 14). Plutôt qu'un effet copié dans chaque page :

ts
// src/hooks/useTitrePage.ts
import { useEffect } from 'react';

// Met à jour le titre de l'onglet : utile à tous, indispensable aux lecteurs d'écran.
export function useTitrePage(titre: string) {
  useEffect(() => {
    document.title = `${titre} | AppliFrais`;
  }, [titre]);
}

Chaque page n'a plus qu'une ligne à écrire :

tsx
export function PageFiche() {
  const { id } = useParams();
  const { data: fiche, isPending, isError, error } = useFiche(Number(id));
  useTitrePage(fiche ? `Fiche de frais – ${formatMois(fiche.mois)}` : 'Fiche de frais');

  if (isPending) return <p role="status">Chargement de la fiche…</p>;
  …
}

Le hook est appelé avant les retours anticipés, comme tous les hooks. Pendant le chargement, le titre est « Fiche de frais | AppliFrais » ; dès que la fiche arrive, il devient « Fiche de frais – octobre 2026 | AppliFrais ».

Un hook qui renvoie un état : useBascule

Un hook peut renvoyer des valeurs et des fonctions, comme useState :

ts
import { useState } from 'react';

export function useBascule(initial = false) {
  const [actif, setActif] = useState(initial);
  const basculer = () => setActif((a) => !a);
  return [actif, basculer] as const;
}

// Dans un composant :
const [ouvert, basculerOuvert] = useBascule();
<button type="button" aria-expanded={ouvert} onClick={basculerOuvert}>Détail</button>

as const dit à TypeScript que le tableau a exactement deux éléments de types différents (un booléen puis une fonction), et non un tableau de « booléen ou fonction ».

useMemo, useCallback et le React Compiler

Tu croiseras deux hooks d'optimisation :

  • useMemo(() => calcul, [dépendances]) garde le résultat d'un calcul coûteux entre les rendus, tant que les dépendances ne changent pas ;
  • useCallback(fonction, [dépendances]) garde la même fonction entre les rendus, pour qu'un composant enfant optimisé ou un effet ne la voie pas « changer ».

Pour une application de gestion, ils sont rarement nécessaires : filtrer quelques dizaines de fiches prend une fraction de milliseconde. Et depuis la version 1.0 du React Compiler (octobre 2025), un plugin de compilation peut ajouter ces optimisations automatiquement ; le modèle de Vite propose de l'activer. Retiens la règle : n'optimise que ce qu'un outil de mesure (l'onglet Profiler des React DevTools) t'a montré comme lent.

Exercices

Exercice 1 sur 4 Où placer l'état ? Réussi À revoir

L'écran de détail d'une fiche affiche un tableau des frais (ListeFrais) et, dans l'en-tête de la page, un compteur « 3 frais sélectionnés ». L'utilisateur coche des frais dans le tableau. Où places-tu l'état des frais sélectionnés, et comment les deux composants y accèdent-ils ?

Voir la réponseMasquer la réponse de l'exercice 1

Dans PageFiche, l'ancêtre commun du compteur et du tableau : const [selection, setSelection] = useState<number[]>([]) (les identifiants cochés). PageFiche affiche le compteur (selection.length) et passe à ListeFrais la sélection et une fonction onBasculer(id). Un Context serait disproportionné : les deux composants sont à un niveau l'un de l'autre.

tsx
function basculer(id: number) {
  setSelection((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
}

Tu avais trouvé ?

Exercice 2 sur 4 Écrire un Context Réussi À revoir

Écris un contexte de préférences d'affichage, PreferencesProvider, qui fournit devise ('EUR' par défaut) et une fonction changerDevise, ainsi que le hook usePreferences() qui lève une erreur claire s'il est utilisé hors du fournisseur.

Voir la réponseMasquer la réponse de l'exercice 2
tsx
import { createContext, use, useState, type ReactNode } from 'react';

type Devise = 'EUR' | 'CHF';
type PreferencesValeur = { devise: Devise; changerDevise: (d: Devise) => void };

const PreferencesContext = createContext<PreferencesValeur | null>(null);

export function PreferencesProvider({ children }: { children: ReactNode }) {
  const [devise, setDevise] = useState<Devise>('EUR');
  return (
    <PreferencesContext value={{ devise, changerDevise: setDevise }}>
      {children}
    </PreferencesContext>
  );
}

export function usePreferences() {
  const valeur = use(PreferencesContext);
  if (valeur === null) throw new Error('usePreferences doit être appelé sous <PreferencesProvider>.');
  return valeur;
}

Un composant affiche ensuite les montants avec new Intl.NumberFormat('fr-FR', { style: 'currency', currency: devise }).

Tu avais trouvé ?

Exercice 3 sur 4 Hook ou fonction ? Réussi À revoir

Lesquelles de ces fonctions doivent être des hooks (et donc commencer par use) ? Justifie.

  1. Une fonction qui calcule le total d'une liste de frais.
  2. Une fonction qui renvoie la largeur de la fenêtre et la met à jour quand on la redimensionne.
  3. Une fonction qui lit l'utilisateur connecté dans le contexte d'authentification.
  4. Une fonction qui formate un mois "2026-10" en « octobre 2026 ».
Voir la réponseMasquer la réponse de l'exercice 3
  1. Une fonction ordinaire (totalFrais) : un calcul pur, sans hook.
  2. Un hook (useLargeurFenetre) : il a besoin d'un état (la largeur) et d'un effet (écouter resize, avec nettoyage).
  3. Un hook (useAuth) : il appelle use ou useContext.
  4. Une fonction ordinaire (formatMois).

Tu avais trouvé ?

Exercice 4 sur 4 Le hook de largeur de fenêtre Réussi À revoir

Écris useLargeurFenetre(), qui renvoie window.innerWidth et se met à jour quand la fenêtre est redimensionnée.

Voir la réponseMasquer la réponse de l'exercice 4
ts
import { useEffect, useState } from 'react';

export function useLargeurFenetre() {
  const [largeur, setLargeur] = useState(() => window.innerWidth);

  useEffect(() => {
    const surRedimension = () => setLargeur(window.innerWidth);
    window.addEventListener('resize', surRedimension);
    return () => window.removeEventListener('resize', surRedimension);
  }, []);

  return largeur;
}

L'effet s'abonne à un système extérieur (la fenêtre du navigateur) et se désabonne au démontage : c'est un usage légitime de useEffect. Pour adapter la mise en page à la largeur, les media queries CSS restent préférables (responsive design) ; ce hook sert quand le comportement change, pas seulement l'apparence.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Authentification en React : connexion, routes protégées, jetons et cookiesRevoir le sommaire du cours