Custom Hooks Utiles
Collection de hooks React réutilisables que j'utilise fréquemment dans mes projets.
useDebounce
Hook pour debouncer une valeur (utile pour les inputs de recherche).
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Utilisation :
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
// Faire l'appel API ici
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Rechercher..."
/>
);
}
useLocalStorage
Hook pour synchroniser un état avec localStorage.
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
// Initialiser avec la valeur du localStorage ou la valeur par défaut
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error loading localStorage key "${key}":`, error);
return initialValue;
}
});
// Wrapper pour sauvegarder dans localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error saving localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
Utilisation :
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
useOnClickOutside
Hook pour détecter les clics en dehors d'un élément (utile pour les dropdowns, modals).
import { useEffect, RefObject } from 'react';
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref?.current;
// Ne rien faire si on clique sur l'élément ou ses enfants
if (!el || el.contains(event.target as Node)) {
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<HTMLDivElement>(null);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && (
<div className="dropdown-menu">
<p>Contenu du dropdown</p>
</div>
)}
</div>
);
}
useMediaQuery
Hook pour réagir aux media queries CSS.
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
// Set initial value
setMatches(media.matches);
// Create listener
const listener = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
// Add listener
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
Utilisation :
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
<div>
{isMobile ? <MobileView /> : <DesktopView />}
{isDarkMode && <p>Mode sombre détecté</p>}
</div>
);
}
usePrevious
Hook pour garder la valeur précédente d'une variable.
import { useRef, useEffect } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Utilisation :
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useAsync
Hook pour gérer les états de chargement asynchrone.
import { useState, useEffect, useCallback } from 'react';
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate = true
) {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: immediate,
error: null,
});
const execute = useCallback(async () => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFunction();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
throw error;
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { ...state, execute };
}
Utilisation :
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useAsync(
() => fetchUser(userId),
true // Exécuter immédiatement
);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!data) return null;
return <div>{data.name}</div>;
}
Bonnes pratiques
- Toujours typer les hooks avec TypeScript
- Mémoriser les callbacks avec
useCallbackpour éviter les re-renders - Nettoyer les effets (timeouts, event listeners, etc.)
- Gérer les erreurs proprement dans les hooks async
- Documenter les hooks complexes avec des exemples
Ressources
- React Hooks Documentation
- useHooks.com - Collection de hooks
- React Hook Form - Pour les formulaires