useState et useEffect couvrent 80% des cas. Mais pour la gestion d’état complexe, les performances et la réutilisabilité, les hooks avancés font une vraie différence.
useReducer : quand useState devient insuffisant
useReducer est préférable à useState dès que l’état a plusieurs sous-valeurs interdépendantes ou quand la logique de transition est complexe.
type CartItem = { id: string; name: string; price: number; quantity: number };
type CartState = { items: CartItem[]; total: number; isLoading: boolean };
type CartAction =
| { type: 'ADD_ITEM'; item: CartItem }
| { type: 'REMOVE_ITEM'; id: string }
| { type: 'UPDATE_QUANTITY'; id: string; quantity: number }
| { type: 'CLEAR_CART' }
| { type: 'SET_LOADING'; loading: boolean };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(i => i.id === action.item.id);
const items = existing
? state.items.map(i => i.id === action.item.id
? { ...i, quantity: i.quantity + 1 } : i)
: [...state.items, { ...action.item, quantity: 1 }];
return { ...state, items, total: items.reduce((sum, i) => sum + i.price * i.quantity, 0) };
}
case 'REMOVE_ITEM': {
const items = state.items.filter(i => i.id !== action.id);
return { ...state, items, total: items.reduce((sum, i) => sum + i.price * i.quantity, 0) };
}
case 'CLEAR_CART':
return { ...state, items: [], total: 0 };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0, isLoading: false });
return (
<div>
{state.items.map(item => (
<div key={item.id}>
{item.name} x{item.quantity}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>
Supprimer
</button>
</div>
))}
<p>Total : {state.total}€</p>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>Vider</button>
</div>
);
}
useContext + useReducer : état global sans Redux
// context/CartContext.tsx
const CartContext = createContext<{
state: CartState;
dispatch: React.Dispatch<CartAction>;
} | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0, isLoading: false });
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
// Hook personnalisé pour consommer le context — with error boundary
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within CartProvider');
return context;
}
// Dans n'importe quel composant
function ProductCard({ product }: { product: Product }) {
const { dispatch } = useCart();
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', item: product })}>
Ajouter au panier
</button>
);
}
useMemo et useCallback : optimiser les re-renders
function ProductList({ products, category, onSelect }: Props) {
// useMemo : recalculer uniquement si products ou category change
const filtered = useMemo(
() => products.filter(p => p.category === category),
[products, category]
);
// useCallback : stable entre les renders si onSelect ne change pas
const handleSelect = useCallback(
(id: string) => {
onSelect(id);
analytics.track('product_selected', { id });
},
[onSelect] // dépendances
);
return (
<>
{filtered.map(product => (
<ProductCard key={product.id} product={product} onSelect={handleSelect} />
))}
</>
);
}
Hooks personnalisés : extraire et réutiliser la logique
// hooks/useApi.ts — hook générique pour les appels API
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(url)
.then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); })
.then(data => { if (!cancelled) setData(data); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; }; // cleanup : éviter les setState sur composant démonté
}, [url]);
return { data, loading, error };
}
// hooks/useLocalStorage.ts
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch { return initialValue; }
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// Usage
function App() {
const { data: products, loading, error } = useApi<Product[]>('/api/products');
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
}
Notre formation React couvre les hooks avancés avec des projets complets.