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
// 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 :
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 :
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) :
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 :
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 :
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 :
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
// 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 :
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 :
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 ->