SmarMaker - Documentation
Docs» 15_training:module4-react-avance:custom-hooks

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 ?

  1. Réutilisation : même logique dans plusieurs composants
  2. Séparation : isoler la logique de l'affichage
  3. Tests : plus facile à tester que des composants
  4. 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

  1. Nom commence par use : obligatoire pour que React applique les règles des hooks
  2. Peut appeler d'autres hooks : useState, useEffect, useCallback, autres custom hooks
  3. Retourne ce dont le composant a besoin : valeurs, fonctions, objets
  4. 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

  1. Custom hook = fonction commençant par use
  2. Encapsule la logique réutilisable entre composants
  3. Chaque appel a son propre état indépendant
  4. Peut appeler d'autres hooks (useState, useEffect, etc.)
  5. Simplifie les composants en extrayant la logique

← Chapitre précédent | Retour au module | Chapitre suivant : Redux →

Previous Next

Made with ❤ by CAP-REL · SmartMaker · GNU AGPL v3+
Code source · Faire un don
SmarMaker - Documentation
Traductions de cette page:
  • Français
  • Deutsch
  • English
  • Español
  • Italiano
  • Nederlands

Table of Contents

Table des matières

  • Chapitre 2 : Custom Hooks
    • Qu'est-ce qu'un Custom Hook ?
    • Pourquoi créer des Custom Hooks ?
    • Exemple : useLocalStorage
    • Exemple : useFetch
    • Exemple : useDebounce
    • Exemple : useToggle
    • Exemple : useForm
    • Exemple : useOnClickOutside
    • Règles pour les Custom Hooks
    • Organisation des fichiers
    • Exercices
      • Exercice 1 : useWindowSize
      • Exercice 2 : usePrevious
    • Points clés à retenir
  • SmartAuth
  • SmartMaker - Back (PHP)
    • Mapping Dolibarr - React
  • SmartMaker - Front (React)
    • Animations de pages
    • Architecture
    • Astuces
    • Calendar
    • Composants et pages
    • Configuration du Provider
    • Debug et Logs
    • Hooks SmartCommon
    • PWA (Progressive Web App)
    • Requêtes API
    • Routage
    • SmartCommon
    • Stockage de données
    • Thèmes
    • Traductions
  • HowTo - Pas à pas - Votre première application
    • Développement PHP (back)
    • Développement React (front)
    • Première étape : Module Builder Dolibarr
    • SmartAuth
    • SmartBoot : Un "squelette" quasiment prêt à l'emploi
  • Formation SmartMaker
    • Module 1 : Fondamentaux JavaScript ES6+
      • Chapitre 1 : Variables et Scope
      • Chapitre 2 : Fonctions
      • Chapitre 3 : Programmation Asynchrone
      • Chapitre 4 : Modules ES6
    • Module 2 : Introduction à React
      • Chapitre 1 : Philosophie React
      • Chapitre 2 : JSX
      • Chapitre 3 : Composants
    • Module 3 : Hooks React Fondamentaux
      • Chapitre 1 : useState
      • Chapitre 2 : useEffect
      • Chapitre 3 : useRef
      • Chapitre 4 : useContext
    • Module 4 : React Avancé
      • Chapitre 1 : useCallback et useMemo
      • Chapitre 2 : Custom Hooks
      • Chapitre 3 : Redux et Redux Toolkit
    • Module 5 : Architecture SmartMaker
      • Chapitre 1 : Structure du projet
      • Chapitre 2 : Configuration
      • Chapitre 3 : Flux de données
    • Module 6 : SmartCommon - Composants
      • Chapitre 1 : Mise en page
      • Chapitre 2 : Navigation
      • Chapitre 3 : Formulaires
      • Chapitre 4 : Affichage
    • Module 7 : SmartCommon - Hooks
      • Chapitre 1 : useApi
      • Chapitre 2 : Gestion d'état
      • Chapitre 3 : Hooks utilitaires
    • Module 8 : Backend API (PHP)
      • Chapitre 1 : Routage
      • Chapitre 2 : Controllers
      • Chapitre 3 : Mappers
      • Extrafields et formulaires dynamiques
    • Module 9 : Intégration complète
      • Chapitre 1 : Backend
      • Chapitre 2 : Frontend
      • Chapitre 3 : Déploiement
    • Module 10 : Fonctionnalités avancées
      • Chapitre 1 : Mode offline
      • Chapitre 2 : Internationalisation (i18n)
      • Chapitre 3 : Autres fonctionnalités
    • Module 11 : Bonnes pratiques
  • Démonstration
  • Start
  • Composants et pages
  • Afficher le texte source
  • Anciennes révisions
  • Liens de retour
  • Haut de page