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.
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 :
- Quels composants utilisent cette donnée ?
- Quel est leur plus proche ancêtre commun ?
- 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 :
<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) :
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.
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 :
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 :
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 proche | Oui | Oui |
Peut s'appeler dans une condition ou après un return | Non (règles des hooks) | Oui |
| Lit aussi une promesse | Non | Oui (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 ?
| Situation | Outil |
|---|---|
| Une donnée sert à un seul composant | useState dans ce composant |
| Deux composants proches doivent la partager | Remonter l'état dans le parent commun, transmettre par les props |
| Elle sert dans des composants éloignés, et change rarement | Un Context |
| Elle vient de l'API | TanStack 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'écrans | Une 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
usesuivi 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 :
// 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 :
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 :
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.
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
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.
- Une fonction qui calcule le total d'une liste de frais.
- Une fonction qui renvoie la largeur de la fenêtre et la met à jour quand on la redimensionne.
- Une fonction qui lit l'utilisateur connecté dans le contexte d'authentification.
- Une fonction qui formate un mois
"2026-10"en « octobre 2026 ».
Voir la réponseMasquer la réponse de l'exercice 3
- Une fonction ordinaire (
totalFrais) : un calcul pur, sans hook. - Un hook (
useLargeurFenetre) : il a besoin d'un état (la largeur) et d'un effet (écouterresize, avec nettoyage). - Un hook (
useAuth) : il appelleuseouuseContext. - 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
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é ?