🪝 Principe des Custom Hooks

Un custom hook est une fonction JavaScript dont le nom commence par use et qui peut appeler d'autres hooks. Il permet d'extraire et de réutiliser la logique stateful entre plusieurs composants.

// Sans custom hook — logique dupliquée dans chaque composant
function Composant1() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return <p>{width}px</p>;
}

// Avec custom hook — logique centralisée et réutilisable
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return width;
}

function Composant1() {
  const width = useWindowWidth();
  return <p>{width}px</p>;
}
function Composant2() {
  const width = useWindowWidth(); // réutilisé !
  return <div style={{ width }}>...</div>;
}

📏 Règles des Custom Hooks

Règles des Hooks :
  1. Nom obligatoirement en use... (convention et ESLint react-hooks)
  2. Appeler uniquement au niveau supérieur (pas dans des conditions ou boucles)
  3. Appeler uniquement depuis des composants React ou d'autres hooks
  4. Chaque appel du hook crée un état indépendant — les données ne sont pas partagées entre instances
// ✅ Correct
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// ✅ Le hook peut retourner n'importe quoi
function useConteur(initial = 0) {
  const [count, setCount] = useState(initial);
  return {
    count,
    increment: () => setCount(c => c + 1),
    decrement: () => setCount(c => c - 1),
    reset: () => setCount(initial),
  };
}

// Usage
function Demo() {
  const [open, toggleOpen] = useToggle();
  const { count, increment, reset } = useConteur(10);
  return (
    <div>
      <button onClick={toggleOpen}>{open ? 'Fermer' : 'Ouvrir'}</button>
      <button onClick={increment}>{count}</button>
    </div>
  );
}

💾 useLocalStorage

Synchronise un état React avec le localStorage — persistance entre les rechargements de page.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored !== null ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.warn('localStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function Preferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark');
  const [lang, setLang] = useLocalStorage('lang', 'fr');
  return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}

🌐 useFetch

Encapsule la logique de requête HTTP avec état de chargement, données et erreur.

import { useState, useEffect, useRef } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json();
      })
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Posts() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
  if (loading) return <p>Chargement...</p>;
  if (error) return <p>Erreur : {error}</p>;
  return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

⏱️ useDebounce

Retarde la mise à jour d'une valeur — idéal pour les recherches en temps réel.

import { useState, useEffect } from 'react';

function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Usage — ne fait la recherche qu'après 400ms d'inactivité
function Recherche() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 400);

  useEffect(() => {
    if (debouncedQuery) {
      console.log('Recherche :', debouncedQuery);
      // fetch('/api/search?q=' + debouncedQuery)
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Rechercher..." />;
}

🔙 usePrevious

import { useRef, useEffect } from 'react';

// Retourne la valeur du rendu précédent
function usePrevious(value) {
  const ref = useRef(undefined);
  useEffect(() => {
    ref.current = value;
  }); // pas de deps array — s'exécute après chaque render
  return ref.current;
}

function Compteur() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Avant : {prevCount ?? '—'} → Maintenant : {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}
📝 Exercices R09 ▶ Mini-projet : Hook Library 🧠 QCM R09 Suivant : Router →