ReactDevFrontend

React Hooks avancés : useReducer, useContext et hooks personnalisés

17 octobre 2026 · Sphinx-Digital

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.