React Context API : createContext, useContext, Provider, useReducer + Context, hooks customs, useMemo, comparaison Zustand/Redux et use() React 19.
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.
Quand passer à une vraie lib de state management
Le Context API tient bien jusqu'à environ une dizaine de contexts indépendants dans
la même application. Au-delà, deux signaux annoncent qu'il faut migrer vers Zustand,
Jotai ou Redux Toolkit : (1) vous passez trop de temps à optimiser les
useMemo pour limiter les re-renders, et (2) vous avez du mal à expliquer
aux nouveaux arrivants quelle valeur vit dans quel context. Ces libs externes ont
résolu le problème : Zustand offre une subscription sélective gratuite, Jotai modélise
chaque valeur comme un atome indépendant, Redux Toolkit fournit une architecture
strict et des devtools puissants.
L'arrivée de use(Promise) dans React 19
React 19 a introduit le hook use() qui peut consommer une Promise ou
un Context. La syntaxe est plus permissive que useContext : on peut
l'appeler dans un if, dans une boucle, ou dans un callback — alors que
useContext exige le scope top-level d'un composant. Avec use(),
des patterns d'accès conditionnel deviennent simples :
const data = condition ? use(MyContext) : defaultValue. C'est l'évolution
naturelle du Context API et la voie recommandée pour les nouveaux composants en 2026.
Comparaison rapide avec Vue Provide/Inject et Angular DI
Le pattern Context React a deux équivalents directs dans les autres frameworks majeurs.
Vue 3 fournit provide('key', value) au parent et inject('key')
au descendant — fonctionne quasi identiquement mais avec une réactivité plus précise
(les changements de valeur ne re-rendent que les composants qui lisent vraiment cette
valeur). Angular utilise la Dependency Injection avec
providers et inject(Token) — beaucoup plus structuré, avec
des injecteurs hiérarchiques et un système de tokens type-safe. Connaître ces trois
approches élargit votre boîte à outils pour choisir le bon pattern par framework.
Mini-projet appliqué — Theme + Auth + i18n via Context
Cas réel d'app SaaS : 3 contextes séparés (thème, authentification, langue) composés à la racine. Chaque contexte expose un Hook custom avec validation Provider, valeur mémoïsée, et types complets. C'est exactement le squelette qu'on retrouve dans Linear, Notion, Vercel Dashboard.
1. ThemeContext — light/dark avec persistance
Pour la persistance via localStorage, voir le comparatif des solutions de stockage.
// contexts/ThemeContext.tsx
type Theme = 'light' | 'dark';
type ThemeContextValue = { theme: Theme; toggle: () => void };
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem('theme') as Theme) ?? 'light'
);
const toggle = useCallback(() => {
setTheme((prev) => {
const next = prev === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', next);
document.documentElement.dataset.theme = next;
return next;
});
}, []);
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}
2. AuthContext — login/logout + user courant
Pour le pattern d'auth tokens recommandé OWASP 2026, voir le mini-projet auth cookies httpOnly + refresh.
// contexts/AuthContext.tsx
type User = { id: string; email: string; role: 'admin' | 'member' };
type AuthState = { status: 'idle' | 'loading' | 'authenticated' | 'error'; user: User | null };
const AuthContext = createContext<{
state: AuthState;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
} | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({ status: 'idle', user: null });
const login = useCallback(async (email: string, password: string) => {
setState({ status: 'loading', user: null });
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
setState({ status: 'error', user: null });
throw new Error('Login failed');
}
const { user } = await res.json();
setState({ status: 'authenticated', user });
}, []);
const logout = useCallback(async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setState({ status: 'idle', user: null });
}, []);
const value = useMemo(() => ({ state, login, logout }), [state, login, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
3. I18nContext — traductions typées
// contexts/I18nContext.tsx
import { translations, type TranslationKey } from '../i18n/translations';
type Lang = 'fr' | 'en' | 'es';
const I18nContext = createContext<{
lang: Lang;
setLang: (l: Lang) => void;
t: (key: TranslationKey) => string;
} | null>(null);
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLang] = useState<Lang>(
() => (navigator.language.startsWith('fr') ? 'fr' : 'en') as Lang
);
const t = useCallback((key: TranslationKey) => translations[lang][key] ?? key, [lang]);
const value = useMemo(() => ({ lang, setLang, t }), [lang, t]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
export function useI18n() {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error('useI18n must be used within I18nProvider');
return ctx;
}
4. Composition à la racine de l'app
// app.tsx
function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<I18nProvider>
<AuthProvider>
{children}
</AuthProvider>
</I18nProvider>
</ThemeProvider>
);
}
// Render
createRoot(document.getElementById('root')!).render(
<AppProviders>
<App />
</AppProviders>
);
// Consommation dans n'importe quel composant — typesafe + autocomplete
function Header() {
const { theme, toggle } = useTheme();
const { state: { user }, logout } = useAuth();
const { t, lang, setLang } = useI18n();
return (
<header data-theme={theme}>
<h1>{t('app.title')}</h1>
{user && <span>{t('hello')}, {user.email}</span>}
<button onClick={toggle}>{theme === 'light' ? '🌙' : '☀️'}</button>
<select value={lang} onChange={(e) => setLang(e.target.value as Lang)}>
<option value="fr">Français</option>
<option value="en">English</option>
</select>
{user && <button onClick={logout}>{t('logout')}</button>}
</header>
);
}
useAuth. (2) Chaque domaine reste indépendant et testable isolément. (3) Si on doit migrer auth vers Zustand pour des perfs, on touche uniquement AuthContext.tsx — les autres contextes restent intacts. (4) Le code est navigable : 1 fichier par domaine fonctionnel. Pour aller plus loin sur la séparation par domaine, lire le comparatif Zustand vs Redux Toolkit.