Gestion du State Global
Partage des données entre composants sans prop drilling.
7.1Le problème : prop drilling
Quand plusieurs composants ont besoin de la même donnée (thème, utilisateur connecté...), tu te retrouves à passer les props à travers plein de niveaux :
TSXtsx
// ❌ PROP DRILLING : on passe "user" à travers tout l'arbre
function App() {
const [user, setUser] = useState({ name: "Alice" })
return <Layout user={user} /> // ← 1
}
function Layout({ user }) {
return <Sidebar user={user} /> // ← 2 (ne l'utilise même pas)
}
function Sidebar({ user }) {
return <UserMenu user={user} /> // ← 3 (ne l'utilise même pas)
}
function UserMenu({ user }) {
return <p>Bonjour {user.name}</p> // ← 4 (enfin utilisé !)
}
// Layout et Sidebar servent juste de "passe-plat"7.2La solution : Context API
Le Context permet de partager des valeurs globalement sans passer par les props :
TSXtsx
import { createContext, useContext, useState } from 'react'
// 1. Créer le Context
const UserContext = createContext<{
name: string
setName: (name: string) => void
} | null>(null)
// 2. Créer le Provider (enveloppe l'app)
function UserProvider({ children }: { children: React.ReactNode }) {
const [name, setName] = useState("Alice")
return (
<UserContext.Provider value={{ name, setName }}>
{children}
</UserContext.Provider>
)
}
// 3. Hook custom pour consommer le Context
function useUser() {
const context = useContext(UserContext)
if (!context) throw new Error("useUser doit être dans un UserProvider")
return context
}TSXtsx
// 4. Utilisation : plus de prop drilling !
function App() {
return (
<UserProvider>
<Layout />
</UserProvider>
)
}
function Layout() {
return <Sidebar /> // ← Plus besoin de passer user !
}
function Sidebar() {
return <UserMenu /> // ← Zéro props
}
function UserMenu() {
const { name, setName } = useUser() // ← Accès direct !
return (
<div>
<p>Bonjour {name}</p>
<button onClick={() => setName("Bob")}>Changer</button>
</div>
)
}7.3Exemple concret : thème sombre/clair
TSXtsx
import { createContext, useContext, useState } from 'react'
type Theme = "light" | "dark"
const ThemeContext = createContext<{
theme: Theme
toggleTheme: () => void
} | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light")
const toggleTheme = () => {
setTheme(prev => prev === "light" ? "dark" : "light")
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={theme === "dark" ? "bg-slate-900 text-white" : "bg-white text-black"}>
{children}
</div>
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error("useTheme doit être dans un ThemeProvider")
return context
}
// Bouton toggle utilisable n'importe où
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === "light" ? "🌙" : "☀️"}
</button>
)
}7.4Combiner Context + useReducer
Pour un state global complexe, combine Context avec useReducer :
TSXtsx
import { createContext, useContext, useReducer } from 'react'
// State & Actions
type CartItem = { id: number; name: string; qty: number }
type CartState = { items: CartItem[] }
type CartAction =
| { type: "ADD"; payload: CartItem }
| { type: "REMOVE"; payload: number }
| { type: "CLEAR" }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case "ADD":
return { items: [...state.items, action.payload] }
case "REMOVE":
return { items: state.items.filter(i => i.id !== action.payload) }
case "CLEAR":
return { items: [] }
}
}
// Context
const CartContext = createContext<{
state: CartState
dispatch: React.Dispatch<CartAction>
} | null>(null)
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] })
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
)
}
export function useCart() {
const ctx = useContext(CartContext)
if (!ctx) throw new Error("useCart doit être dans CartProvider")
return ctx
}
// Utilisation
function Panier() {
const { state, dispatch } = useCart()
return (
<div>
<h2>Panier ({state.items.length})</h2>
{state.items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => dispatch({ type: "REMOVE", payload: item.id })}>
Retirer
</button>
</div>
))}
<button onClick={() => dispatch({ type: "CLEAR" })}>
Vider le panier
</button>
</div>
)
}