Chapitre 2 : Custom Hooks
Qu'est-ce qu'un Custom Hook ?
Un custom hook est une fonction JavaScript qui :
- Commence par
use(convention obligatoire) - Peut appeler d'autres hooks
- Encapsule de la logique réutilisable
- snippet.javascript
// Custom hook function useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue); const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); const reset = () => setCount(initialValue); return { count, increment, decrement, reset }; } // Utilisation function Counter() { const { count, increment, decrement, reset } = useCounter(10); return ( <div> <p>{count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> ); }
Pourquoi créer des Custom Hooks ?
- Réutilisation : même logique dans plusieurs composants
- Séparation : isoler la logique de l'affichage
- Tests : plus facile à tester que des composants
- Lisibilité : composants plus simples
Exemple : useLocalStorage
Hook qui synchronise l'état avec localStorage :
- snippet.javascript
function useLocalStorage(key, initialValue) { // Initialisation depuis localStorage const [value, setValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); // Mise à jour localStorage quand la valeur change useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(error); } }, [key, value]); return [value, setValue]; } // Utilisation function Settings() { const [theme, setTheme] = useLocalStorage('theme', 'light'); const [language, setLanguage] = useLocalStorage('language', 'fr'); return ( <div> <select value={theme} onChange={e => setTheme(e.target.value)}> <option value="light">Clair</option> <option value="dark">Sombre</option> </select> <select value={language} onChange={e => setLanguage(e.target.value)}> <option value="fr">Français</option> <option value="en">English</option> </select> </div> ); }
Exemple : useFetch
Hook pour les appels API :
- snippet.javascript
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const fetchData = async () => { setLoading(true); setError(null); try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const json = await response.json(); if (!cancelled) { setData(json); } } catch (err) { if (!cancelled) { setError(err.message); } } finally { if (!cancelled) { setLoading(false); } } }; fetchData(); return () => { cancelled = true; }; }, [url]); return { data, loading, error }; } // Utilisation function UserProfile({ userId }) { const { data: user, loading, error } = useFetch(`/api/users/${userId}`); if (loading) return <p>Chargement...</p>; if (error) return <p>Erreur : {error}</p>; return <div>{user.name}</div>; }
Exemple : useDebounce
Hook pour retarder une valeur (utile pour la recherche) :
- snippet.javascript
function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; } // Utilisation function SearchInput({ onSearch }) { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300); useEffect(() => { if (debouncedQuery) { onSearch(debouncedQuery); } }, [debouncedQuery, onSearch]); return ( <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Rechercher..." /> ); }
Exemple : useToggle
Hook simple pour les valeurs booléennes :
- snippet.javascript
function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue(v => !v); }, []); const setTrue = useCallback(() => setValue(true), []); const setFalse = useCallback(() => setValue(false), []); return { value, toggle, setTrue, setFalse }; } // Utilisation function Modal() { const { value: isOpen, toggle, setFalse: close } = useToggle(); return ( <div> <button onClick={toggle}>Ouvrir/Fermer</button> {isOpen && ( <div className="modal"> <p>Contenu du modal</p> <button onClick={close}>Fermer</button> </div> )} </div> ); }
Exemple : useForm
Hook pour gérer les formulaires :
- snippet.javascript
function useForm(initialValues) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const handleChange = useCallback((e) => { const { name, value, type, checked } = e.target; setValues(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })); }, []); const setValue = useCallback((name, value) => { setValues(prev => ({ ...prev, [name]: value })); }, []); const reset = useCallback(() => { setValues(initialValues); setErrors({}); }, [initialValues]); const validate = useCallback((validationRules) => { const newErrors = {}; for (const [field, rules] of Object.entries(validationRules)) { for (const rule of rules) { const error = rule(values[field], values); if (error) { newErrors[field] = error; break; } } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }, [values]); return { values, errors, handleChange, setValue, reset, validate }; } // Utilisation function LoginForm({ onSubmit }) { const { values, errors, handleChange, validate } = useForm({ email: '', password: '' }); const validationRules = { email: [ v => !v && 'Email requis', v => !v.includes('@') && 'Email invalide' ], password: [ v => !v && 'Mot de passe requis', v => v.length < 6 && 'Minimum 6 caractères' ] }; const handleSubmit = (e) => { e.preventDefault(); if (validate(validationRules)) { onSubmit(values); } }; return ( <form onSubmit={handleSubmit}> <div> <input name="email" value={values.email} onChange={handleChange} placeholder="Email" /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <input name="password" type="password" value={values.password} onChange={handleChange} placeholder="Mot de passe" /> {errors.password && <span className="error">{errors.password}</span>} </div> <button type="submit">Connexion</button> </form> ); }
Exemple : useOnClickOutside
Hook pour détecter les clics en dehors d'un élément :
- snippet.javascript
function useOnClickOutside(ref, handler) { useEffect(() => { const listener = (event) => { if (!ref.current || ref.current.contains(event.target)) { return; } handler(event); }; document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener); return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, [ref, handler]); } // Utilisation function Dropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); useOnClickOutside(dropdownRef, () => setIsOpen(false)); return ( <div ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)}>Menu</button> {isOpen && ( <ul className="dropdown-menu"> <li>Option 1</li> <li>Option 2</li> <li>Option 3</li> </ul> )} </div> ); }
Règles pour les Custom Hooks
- Nom commence par
use: obligatoire pour que React applique les règles des hooks - Peut appeler d'autres hooks : useState, useEffect, useCallback, autres custom hooks
- Retourne ce dont le composant a besoin : valeurs, fonctions, objets
- Chaque appel est indépendant : deux composants utilisant le même hook ont des états séparés
Organisation des fichiers
src/
hooks/
useLocalStorage.js
useFetch.js
useDebounce.js
useToggle.js
useForm.js
index.js // export tous les hooks
- snippet.javascript
// hooks/index.js export { useLocalStorage } from './useLocalStorage'; export { useFetch } from './useFetch'; export { useDebounce } from './useDebounce'; export { useToggle } from './useToggle'; export { useForm } from './useForm';
Exercices
Exercice 1 : useWindowSize
Créer un hook qui retourne les dimensions de la fenêtre et se met à jour au redimensionnement.
Solution :
- snippet.javascript
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return size; } // Utilisation function ResponsiveComponent() { const { width, height } = useWindowSize(); return ( <div> Fenêtre : {width} x {height} {width < 768 && <p>Mode mobile</p>} </div> ); }
Exercice 2 : usePrevious
Créer un hook qui retourne la valeur précédente d'une variable.
Solution :
- snippet.javascript
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } // Utilisation function Counter() { const [count, setCount] = useState(0); const previousCount = usePrevious(count); return ( <div> <p>Actuel : {count}</p> <p>Précédent : {previousCount}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); }
Points clés à retenir
- Custom hook = fonction commençant par
use - Encapsule la logique réutilisable entre composants
- Chaque appel a son propre état indépendant
- Peut appeler d'autres hooks (useState, useEffect, etc.)
- Simplifie les composants en extrayant la logique
← Chapitre précédent | Retour au module | Chapitre suivant : Redux →