Context API & useReducer

Résoudre le prop drilling et gérer le state global

Utilisez les flĂšches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Comprendre le prop drilling

Le problĂšme des props Ă  travers 5 niveaux

2. Context API

Créer, fournir et consommer un contexte

3. useReducer

Gérer un state complexe avec des actions

4. Pattern Context + useReducer

La solution ultime pour le state global

Plan du cours

1

Le problĂšme : Prop Drilling

Passer des props Ă  travers 5 composants... et souffrir

2

La solution : Context API

createContext, Provider, useContext

3

useReducer : le state intelligent

Redux simplifié dans votre composant

4

Pattern : Context + useReducer

La combinaison parfaite pour le state global

5

Demo live : Cart avec Context

Construire un panier d'achat ensemble

Prop drilling : Le cauchemar

// App possĂšde l'utilisateur

function App() {

const user = { name: "Alice" };

return <Layout user={user} />;

}

function Layout({ user }) {

// Layout ne fait que transmettre!

return <Sidebar user={user} />;

}

function Sidebar({ user }) {

// Sidebar ne fait que transmettre!

return <UserMenu user={user} />;

}

function UserMenu({ user }) {

// Enfin! On utilise la prop!

return <span>{user.name}</span>;

}

ProblĂšme : Layout et Sidebar ne font que transmettre

Si on ajoute un niveau, il faut modifier TOUTE la chaĂźne

Pourquoi c'est problématique ?

Maintenance difficile

Changer le nom d'une prop = modifier tous les composants intermédiaires

Code verbose

Des props inutiles partout, le code devient illisible

Refactoring risqué

Ajouter/retirer un niveau casse la chaĂźne

Solution : Context API - créer un "tunnel" direct

Context API

Le "tunnel" qui traverse la hiérarchie

Context permet de transmettre des données à n'importe quel niveau

Sans passer par chaque composant intermédiaire

Les 3 éléments du Context

createContext

Créer le contexte

const MyContext =

createContext();

Provider

Fournir la valeur

<MyContext.Provider

value={data}>

useContext

Consommer la valeur

const value =

useContext(MyContext);

Ces 3 éléments travaillent ensemble pour créer un "tunnel" de données

createContext : Créer le contexte

import { createContext } from 'react';

// Créer un contexte avec une valeur par défaut

const ThemeContext = createContext('light');

La valeur par défaut est utilisée si aucun Provider n'existe

Provider : Fournir la valeur

function App() {

const [theme, setTheme] = useState('light');

return (

<ThemeContext.Provider value={{ theme, setTheme }}>

<Layout />

</ThemeContext.Provider>

);

}

Tous les enfants de Provider peuvent accéder à la valeur

useContext : Consommer la valeur

import { useContext } from 'react';

function ThemedButton() {

const { theme, setTheme } = useContext(ThemeContext);

return (

<button

className={theme === 'dark' ? 'dark-btn' : 'light-btn'}

onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>

Toggle Theme

</button>

);

}

Le composant accĂšde directement au contexte, sans props!

Bonne pratique : hook personnalisé

Éviter l'import direct

const { theme } =

useContext(ThemeContext);

Répétitif et sujet aux erreurs

Créer un hook dédié

const { theme } = useTheme();

Import plus simple, erreur claire si hors Provider

function useTheme() {

const context = useContext(ThemeContext);

if (!context) {

throw new Error('useTheme must be used within ThemeProvider');

}

return context;

}

Exemple complet : ThemeContext

// ThemeContext.jsx

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {

const [theme, setTheme] = useState('light');

return (

<ThemeContext.Provider value={{ theme, setTheme }}>

{children}

</ThemeContext.Provider>

);

}

export function useTheme() {

const context = useContext(ThemeContext);

if (!context) throw new Error('useTheme must be within ThemeProvider');

return context;

}

Utilisation dans l'application

// App.jsx - Envelopper avec le Provider

import { ThemeProvider } from './ThemeContext';

function App() {

return (

<ThemeProvider>

<Header />

<Main />

</ThemeProvider>

);

}

// Header.jsx - Utiliser le hook

import { useTheme } from './ThemeContext';

function Header() {

const { theme, setTheme } = useTheme();

return <button onClick={() => setTheme(...)}>...</button>;

}

Demo : AuthContext

const AuthContext = createContext(null);

export function AuthProvider({ children }) {

const [user, setUser] = useState(null);

const login = async (email, password) => {

const response = await fetch('/api/login', {...});

setUser(response.user);

};

const logout = () => setUser(null);

return (

<AuthContext.Provider value={{ user, login, logout }}>

{children}

</AuthContext.Provider>

);

}

AuthContext expose : user, login(), logout()

Tous les composants peuvent accéder à l'authentification

useReducer

Le state management intelligent

useReducer est une alternative Ă  useState

Pour une logique de state complexe

useState vs useReducer

useState

Simple et direct

const [count, setCount]

= useState(0);

Idéal pour : valeur unique, logique simple

useReducer

Structuré et puissant

const [state, dispatch]

= useReducer(reducer,

initialState);

Idéal pour : state complexe, multiples actions

Quand utiliser useReducer ?

Utiliser useReducer si...

  • State complexe (plusieurs sous-valeurs)
  • Multiples actions (add, remove, update...)
  • Logique de mise Ă  jour Ă©laborĂ©e
  • Besoin de prĂ©visibilitĂ© (tests)

Préférer useState si...

  • Valeur simple (nombre, string)
  • Une seule façon de mettre Ă  jour
  • Logique triviale
  • Pas besoin de pattern prĂ©visible

Analogie : Redux en miniature

// Redux

store.dispatch({ type: 'INCREMENT' });

// useReducer - mĂȘme pattern!

dispatch({ type: 'INCREMENT' });

useReducer = Redux dans un composant

MĂȘme architecture, sans la complexitĂ©

Syntaxe de useReducer

const [state, dispatch] = useReducer(reducer, initialState);

reducer

Fonction pure

(state, action) => newState

initialState

Valeur initiale

Objet ou primitive

dispatch

Envoyer une action

dispatch({ type: 'X' })

dispatch() envoie une action au reducer

Le reducer calcule le nouveau state et React re-render

Le reducer : une fonction pure

function reducer(state, action) {

switch (action.type) {

case 'INCREMENT':

return { count: state.count + 1 };

case 'DECREMENT':

return { count: state.count - 1 };

case 'RESET':

return { count: 0 };

default:

return state;

}

}

Le reducer est une fonction pure : pas d'effets de bord!

MĂȘme input = mĂȘme output, pas de mutations

ParallĂšle : Backend Event-Sourcing

Backend

// Events stockés

[{ type: 'USER_CREATED' },

{ type: 'EMAIL_UPDATED' },

{ type: 'USER_DELETED' }]

State reconstruit en replayant les events

Frontend (useReducer)

// Actions dispatchées

dispatch({ type: 'ADD_ITEM' });

dispatch({ type: 'REMOVE_ITEM' });

dispatch({ type: 'CLEAR_CART' });

State recalculé par le reducer

(state, action) => newState

La mĂȘme fonction pure dans les deux cas!

Exemple complet : Compteur

const initialState = { count: 0 };

function reducer(state, action) {

switch (action.type) {

case 'increment': return { count: state.count + 1 };

case 'decrement': return { count: state.count - 1 };

case 'reset': return initialState;

default: throw new Error('Unknown action');

}

}

function Counter() {

const [state, dispatch] = useReducer(reducer, initialState);

return (

<>

<p>Count: {state.count}</p>

<button onClick={() => dispatch({ type: 'increment' })}>+</button>

<button onClick={() => dispatch({ type: 'decrement' })}>-</button>

</>

);

}

Pattern ultime

Context + useReducer

La combinaison pour le state global

Context pour la portée, useReducer pour la logique

Pourquoi les combiner ?

Context seul

Bien pour des valeurs simples

Mais useState dans le Provider = re-renders fréquents

useReducer seul

Bien pour la logique

Mais limité à un seul composant

Ensemble = puissance maximale

useReducer gĂšre le state, Context le partage partout

Pattern : Context + useReducer

const CartContext = createContext();

function CartProvider({ children }) {

const [state, dispatch] = useReducer(cartReducer, { items: [] });

return (

<CartContext.Provider value={{ state, dispatch }}>

{children}

</CartContext.Provider>

);

}

Le Provider expose state ET dispatch

Les composants peuvent lire le state et envoyer des actions

Exemple : Cart Reducer

function cartReducer(state, action) {

switch (action.type) {

case 'ADD_ITEM':

return { items: [...state.items, action.payload] };

case 'REMOVE_ITEM':

return { items: state.items.filter(i => i.id !== action.payload) };

case 'CLEAR_CART':

return { items: [] };

default:

return state;

}

}

Actions typiques d'un cart : ADD_ITEM, REMOVE_ITEM, CLEAR_CART

Utilisation du CartContext

function ProductCard({ product }) {

const { dispatch } = useCart();

const addToCart = () => {

dispatch({ type: 'ADD_ITEM', payload: product });

};

return <button onClick={addToCart}>Ajouter</button>;

}

function CartBadge() {

const { state } = useCart();

return <span>{state.items.length} articles</span>;

}

ProductCard dispatch des actions, CartBadge lit le state

Séparation claire des responsabilités

Pattern avancé : Split contexts

const CartStateContext = createContext();

const CartDispatchContext = createContext();

function CartProvider({ children }) {

const [state, dispatch] = useReducer(cartReducer, initialState);

return (

<CartStateContext.Provider value={state}>

<CartDispatchContext.Provider value={dispatch}>

{children}

</CartDispatchContext.Provider>

</CartStateContext.Provider>

);

}

Les composants qui dispatchent seulement ne re-renderent pas quand le state change!

Bonnes pratiques

Faire

  • Un contexte par domaine
  • CrĂ©er des hooks personnalisĂ©s
  • Typer les actions avec TypeScript
  • Documenter le reducer

Éviter

  • Un seul contexte gĂ©ant
  • Mettre TOUT dans le contexte
  • Oublier la valeur par dĂ©faut
  • Muter le state dans le reducer

Erreur commune : un seul gros contexte

L'erreur

<AppContext.Provider

value={ user, theme,

cart, notifications }>

Un seul contexte pour tout l'app

La solution

<AuthProvider>

<ThemeProvider>

<CartProvider>

{children}

</CartProvider>

</ThemeProvider>

</AuthProvider>

Séparer par domaine

Résumé

Context API

Tunnel pour les données

createContext, Provider, useContext

useReducer

State management intelligent

reducer, dispatch, actions

Pattern combiné

State global optimal

Context + useReducer

Questions ?

PrĂȘts pour la demo live!