Flux
Flux de données unidirectionnel dans les SPAs
Problème
Dans une SPA avec beaucoup d'état partagé, les mutations viennent de partout : composants, effets, websockets, callbacks. Tracer la cause d'un bug d'état devient difficile quand n'importe qui peut modifier n'importe quoi.
Le principe
Flux impose un flux de données unidirectionnel :
Action → Dispatcher → Store → View → Action → ...- Action : description de ce qui s'est passé (
{ type: 'ADD_TO_CART', payload: item }) - Dispatcher : achemine l'action vers le bon store
- Store : contient l'état et la logique de mise à jour
- View : lit le store et émet des actions en réponse aux interactions
L'état ne se modifie que via des actions. La vue ne modifie jamais l'état directement.
Redux : l'implémentation historique
Redux est l'implémentation Flux la plus utilisée. Elle ajoute la contrainte que le store est une fonction pure (reducer).
// Action
const addToCart = (item: CartItem) => ({
type: 'cart/add' as const,
payload: item,
});
// Reducer (fonction pure : même input = même output)
function cartReducer(state: CartState = { items: [] }, action: CartAction): CartState {
switch (action.type) {
case 'cart/add':
return { items: [...state.items, action.payload] };
case 'cart/remove':
return { items: state.items.filter(i => i.id !== action.payload) };
default:
return state;
}
}Chaque mutation est traçable : les DevTools Redux permettent de rejouer l'historique des actions une par une.
useReducer : Flux natif en React
useReducer est Flux sans librairie externe. Adapté quand l'état est local à un composant ou un sous-arbre.
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' };
function counterReducer(state: number, action: Action): number {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
}
}
function Counter() {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}Zustand
Zustand garde le flux unidirectionnel sans le boilerplate de Redux. L'état et les actions sont colocalisés dans le store.
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
add: (item: CartItem) => void;
remove: (id: string) => void;
clear: () => void;
}
const useCartStore = create<CartStore>((set) => ({
items: [],
add: (item) => set((state) => ({ items: [...state.items, item] })),
remove: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
clear: () => set({ items: [] }),
}));
// Les composants lisent et dispatchent via le même hook
function CartButton({ item }: { item: CartItem }) {
const add = useCartStore((state) => state.add);
return <button onClick={() => add(item)}>Ajouter</button>;
}Quand utiliser quoi
| Situation | Solution |
|---|---|
| État local à un composant | useState |
| État local complexe avec plusieurs cas | useReducer |
| État global partagé, app de taille moyenne | Zustand |
| Besoin de DevTools, time-travel debugging | Redux Toolkit |