🪝 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 :
- Nom obligatoirement en
use...(convention et ESLint react-hooks) - Appeler uniquement au niveau supérieur (pas dans des conditions ou boucles)
- Appeler uniquement depuis des composants React ou d'autres hooks
- 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>
);
}