Custom Hooks
Les custom hooks vous permettent de reutiliser la logique avec état (stateful) entre differents composants.
Information
Un custom hook, c'est quoi ?
C'est une fonction JavaScript qui utilise des hooks React (useState, useEffect, etc.) et qui peut etre reutilisee dans plusieurs composants.
Pourquoi créer des custom hooks ?
Problème : Code dupliqué
Sans custom hooks, vous dupliquez la logique :
// ❌ Composant 1 : Fetch users
function UsersList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ❌ Composant 2 : Fetch products (même logique !)
function ProductsList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/products') // Seule différence
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Solution : Custom Hook
Extrayez la logique commune dans un custom hook :
// ✅ hooks/useFetch.js
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
export default useFetch;
Utilisation
// ✅ Composant 1 : Beaucoup plus simple !
function UsersList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Composant 2 : Même simplicité !
function ProductsList() {
const { data: products, loading, error } = useFetch('/api/products');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{products?.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
Succès
Avantages :
- Code DRY (Don't Repeat Yourself)
- Logique centralisee et testable
- Composants plus lisibles
- Facilite les modifications (un seul endroit a changer)
Qu'est-ce qu'un custom hook ?
Regles des custom hooks
Les custom hooks doivent suivre les mêmes règles que les hooks React :
Regles importantes
- Nom doit commencer par "use" (convention obligatoire)
- Appeles au top level (pas dans des conditions/boucles)
- Appeles uniquement dans les composants ou autres hooks
- Peuvent utiliser d'autres hooks (useState, useEffect, etc.)
Quelle convention de nommage est obligatoire pour les custom hooks ?
Exemples de custom hooks utiles
1. useLocalStorage
Persiste l'état dans le localStorage automatiquement.
// hooks/useLocalStorage.js
function useLocalStorage(key, initialValue) {
// État initialisé avec la valeur du localStorage (si elle existe)
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Sauvegarde dans localStorage à chaque changement
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
export default useLocalStorage;
Utilisation
function TodoApp() {
// ✅ Les todos sont automatiquement sauvegardés et chargés !
const [todos, setTodos] = useLocalStorage('todos', []);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, completed: false }]);
};
return (
<div>
<h1>Mes Todos ({todos.length})</h1>
{/* ... */}
</div>
);
}
2. useToggle
Simplifie la gestion d'un booléen (show/hide, on/off, etc.).
// hooks/useToggle.js
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, toggle, setTrue, setFalse];
}
export default useToggle;
Utilisation
function Modal() {
const [isOpen, toggle, open, close] = useToggle(false);
return (
<>
<button onClick={open}>Ouvrir</button>
{isOpen && (
<div className="modal">
<p>Contenu du modal</p>
<button onClick={close}>Fermer</button>
</div>
)}
</>
);
}
3. useDebounce
Retarde la mise à jour d'une valeur (utile pour les recherches).
// hooks/useDebounce.js
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Créer un timer
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Nettoyer le timer si value change avant la fin du délai
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Utilisation
function SearchProducts() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data: products } = useFetch(`/api/products?search=${debouncedSearchTerm}`);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Rechercher..."
/>
{/* L'API est appelée seulement 500ms après l'arrêt de la saisie */}
<ProductList products={products} />
</div>
);
}
Astuce
Le debounce evite d'appeler l'API a chaque frappe. L'API est appelee seulement apres que l'utilisateur a arrete de taper pendant le delai specifie (ici 500ms).
Que retourne generalement un custom hook ?
4. useForm
Simplifie la gestion de formulaires.
// hooks/useForm.js
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
};
const reset = () => {
setValues(initialValues);
};
return { values, handleChange, reset };
}
export default useForm;
Utilisation
function ContactForm() {
const { values, handleChange, reset } = useForm({
name: '',
email: '',
message: ''
});
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form data:', values);
reset();
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={values.name}
onChange={handleChange}
placeholder="Nom"
/>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={values.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Envoyer</button>
</form>
);
}
Améliorer useFetch
Notre premier hook useFetch était simple. Améliorons-le !
Version avancée
// hooks/useFetch.js
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Éviter la mise à jour si le composant est démonté
let isCancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (!isCancelled) {
setData(json);
setLoading(false);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
// Cleanup function
return () => {
isCancelled = true;
};
}, [url]); // Re-fetch si l'URL change
return { data, loading, error };
}
export default useFetch;
Information
Améliorations :
- Utilise async/await (plus lisible)
- Vérifie le status HTTP
- Évite les mises à jour sur un composant démonté
- Re-fetch automatiquement si l'URL change
Composer des hooks
Les custom hooks peuvent utiliser d'autres custom hooks !
// hooks/useProducts.js
function useProducts() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 300);
const { data, loading, error } = useFetch(
`/api/products?search=${debouncedSearch}`
);
return {
products: data,
loading,
error,
searchTerm,
setSearchTerm
};
}
export default useProducts;
Utilisation
function ProductsPage() {
const { products, loading, error, searchTerm, setSearchTerm } = useProducts();
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Rechercher des produits..."
/>
{loading && <p>Chargement...</p>}
{error && <p>Erreur : {error}</p>}
{products && <ProductList products={products} />}
</div>
);
}
Succès
Le composant reste concis. Toute la logique de fetch, debounce et recherche est dans les hooks.
Bonnes pratiques
1. Un hook = une responsabilité
// ✅ Bon - Chaque hook a un rôle précis
useFetch('/api/users')
useLocalStorage('theme', 'light')
useDebounce(searchTerm, 500)
// ❌ Mauvais - Hook qui fait trop de choses
useEverything() // Fetch, storage, debounce, etc.
2. Retourner des objets pour la flexibilité
// ✅ Bon - Destructuration nommée
const { data, loading, error } = useFetch(url);
// ✅ Bon aussi - Renommage facile
const { data: users, loading: usersLoading } = useFetch('/api/users');
// ❌ Moins flexible - Ordre imposé
const [data, loading, error] = useFetch(url);
3. Gérer les cas limites
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// ✅ Vérifier que l'URL existe
if (!url) {
setLoading(false);
return;
}
// ... fetch logic
}, [url]);
return { data, loading, error };
}
Pourquoi retourner un objet plutot qu'un tableau dans un custom hook ?
4. Documenter vos hooks
/**
* Hook pour fetch des données depuis une API
*
* @param {string} url - L'URL à fetch
* @param {object} options - Options fetch (optionnel)
* @returns {object} { data, loading, error }
*
* @example
* const { data: users, loading } = useFetch('/api/users');
*/
function useFetch(url, options = {}) {
// ...
}
Exercice pratique
Créez votre propre hook
Créez un hook useCounter qui :
- Gère un compteur (valeur numérique)
- Fournit des fonctions increment, decrement, reset
- Accepte une valeur initiale et optionnellement un pas (step)
Signature :
const { count, increment, decrement, reset } = useCounter(initialValue, step);
Exemple d'utilisation :
function Counter() {
const { count, increment, decrement, reset } = useCounter(0, 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Récapitulatif
Points clés
✅ Les custom hooks permettent de :
- Réutiliser la logique stateful
- Simplifier les composants
- Organiser le code de manière modulaire
- Composer des comportements complexes
✅ Règles :
- Nom commence par "use"
- Utilisent des hooks React
- Appelés au top level seulement
✅ Exemples courants :
- useFetch - API calls
- useLocalStorage - Persistence
- useDebounce - Optimisation
- useForm - Formulaires
- useToggle - États booléens
Pour aller plus loin
Bibliothèques de custom hooks :
Ressources :