10

Custom Hooks

Créer et partager sa propre logique métier

Le concept : Que faire lorsque deux composants différents ont besoin de la même logique avec état (comme écouter la taille de la fenêtre ou gérer une requête réseau) ?

C'est là qu'interviennent les Custom Hooks. Ce sont de simples fonctions JavaScript (commençant par use...) qui encapsulent useState, useEffect ou d'autres hooks. Ils permettent d'extraire la logique complexe hors de votre UI pour la rendre réutilisable et testable.

useFetch() useState useEffect data
💡 Pourquoi créer ses propres Hooks ?

Un Custom Hook est une simple fonction JavaScript dont le nom commence par use et qui peut appeler d'autres Hooks React. C'est le mécanisme principal pour extraire et partager de la logique avec état entre plusieurs composants, sans dupliquer du code.

Avantages clés :

  • DRY : Écrire la logique une fois, l'utiliser partout.
  • Testabilité : Tester la logique séparément des composants.
  • Lisibilité : Les composants restent propres, focalisés sur l'affichage.
Structure d'un Custom Hook
// hooks/useCounter.js
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);

    const increment = useCallback(() => setCount(n => n + 1), []);
    const decrement = useCallback(() => setCount(n => n - 1), []);
    const reset     = useCallback(() => setCount(initialValue), [initialValue]);

    return { count, increment, decrement, reset };
}

// Dans un composant
function Counter() {
    const { count, increment, reset } = useCounter(10);
    return <div>{count} <button onClick={increment}>+</button></div>;
}

🛠️ Hooks utilitaires essentiels

useFetch — Chargement de données
function useFetch(url) {
    const [data, setData]       = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError]     = useState(null);

    useEffect(() => {
        let ignore = false;
        setLoading(true);

        fetch(url)
            .then(r => r.json())
            .then(d => { if (!ignore) setData(d); })
            .catch(e => { if (!ignore) setError(e); })
            .finally(() => { if (!ignore) setLoading(false); });

        return () => { ignore = true; };
    }, [url]);

    return { data, loading, error };
}

// Utilisation
const { data, loading } = useFetch("/api/users");
useDebounce — Limiter les appels
function useDebounce(value, delay = 300) {
    const [debounced, setDebounced] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => {
            setDebounced(value);
        }, delay);

        return () => clearTimeout(timer);
    }, [value, delay]);

    return debounced;
}

// Utilisation : recherche en différé
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 500);

// L'API n'est appelée que 500ms après la dernière frappe
const { data } = useFetch(`/search?q=${debouncedQuery}`);
useLocalStorage — Persistance
function useLocalStorage(key, initial) {
    const [value, setValue] = useState(() => {
        try {
            const stored = localStorage.getItem(key);
            return stored ? JSON.parse(stored) : initial;
        } catch { return initial; }
    });

    const setStored = useCallback((val) => {
        setValue(val);
        localStorage.setItem(key, JSON.stringify(val));
    }, [key]);

    return [value, setStored];
}

// Comme useState mais persistant
const [theme, setTheme] = useLocalStorage('theme', 'dark');
useWindowSize — Responsive JS
function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });

    useEffect(() => {
        const handler = () => setSize({
            width: window.innerWidth,
            height: window.innerHeight
        });
        window.addEventListener('resize', handler);
        return () => window.removeEventListener('resize', handler);
    }, []);

    return size;
}

// Dans un composant
const { width } = useWindowSize();
const isMobile = width < 768;
💡 Convention de nommage : Commencez toujours par use (ex: useAuth, useForm, useCart). React Linting vérifie cette convention pour s'assurer que les règles des Hooks sont respectées.
📚 Bibliothèques de Hooks populaires : react-query / TanStack Query (data fetching), react-hook-form (formulaires), zustand (state global), usehooks-ts (utilitaires TypeScript).