Accueil / Articles / Un ensemble d’hooks personnalisables réutilisables pour chaque nouveau projet React

Un ensemble d’hooks personnalisables réutilisables pour chaque nouveau projet React

Découvrez un ensemble soigneusement sélectionné de hooks React personnalisés — couvrant le stockage, le débouncing, les clics et la récupération de données — qui éliminent le code générique répétitif dans les nouveaux projets.

3840 mots

Les hooks React sont devenus la méthode par défaut pour gérer l’état, les effets secondaires et la logique réutilisable dans les applications modernes, mais connaître les hooks intégrés n’est que la moitié de l’affaire. Tout aussi important est d’avoir un ensemble personnel de petits hooks personnalisés, bien testés, que l’on peut intégrer dans n’importe quel nouveau projet afin d’éviter de reconstruire les mêmes outils à partir de zéro. Cet article passe d’abord en revue une série de hooks à inclure dans votre kit de démarrage, puis explore plus en détail le fonctionnement réel des API de hooks pour que vous compreniez non seulement ce à copier, mais aussi pourquoi ils se comportent de cette manière.

Lors du démarrage d’un nouveau projet React, certains hooks ont tendance à réapparaître presque à chaque fois. Certains traitent des problèmes de performance, d’autres améliorent l’expérience utilisateur, et plusieurs empêchent simplement de réécrire la même logique de base dans chaque codebase. Au fil des projets, ces outils deviennent un ensemble de départ standard : au lieu de résoudre le même problème à partir de zéro, on intègre le hook, on l’adapte si le projet nécessite quelque chose de légèrement différent, puis on reprend la création des fonctionnalités réelles. Voici douze hooks qui correspondent à cette description.

Le premier est useLocalStorage, utile car presque toutes les applications ont besoin de conserver des données entre les recharges, qu’il s’agisse d’une préférence de thème, des données d’un formulaire ou d’autres paramètres utilisateur.

import { useState } from "react";
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });  const updateValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };  return [value, updateValue];
}

Ce schéma est souvent utilisé pour stocker des éléments tels que les préférences de mode sombre, un jeton d’authentification ou tout paramètre que vous souhaitez conserver entre les visites.

Ensuite vient useToggle, adapté aux cas courants où un état passe simplement de true à false.

import { useState } from "react";
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);  const toggle = () => setValue(v => !v);  return [value, toggle];
}

Ce mécanisme convient parfaitement aux modaux, aux menus déroulants, aux barres latérales et à d’autres éléments d’interface qui s’activent/ désactivent.

useDebounce est fréquemment utilisé autour des champs de recherche, afin d’éviter d’envoyer une requête à chaque frappe.

import { useState, useEffect } from "react";
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(value);
    }, delay);    return () => clearTimeout(timer);
  }, [value, delay]);  return debounced;
}

En différant la mise à jour jusqu’à ce qu’il n’y ait plus de frappes, on réduit le nombre inutile d’appels à l’API.

useWindowSize permet aux layouts réactifs d’accéder aux dimensions actuelles de la zone de visualisation.

import { useState, useEffect } from "react";
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });  useEffect(() => {
    const resize = () =>
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });    window.addEventListener("resize", resize);    return () => window.removeEventListener("resize", resize);
  }, []);  return size;
}

usePrevious est utile chaque fois qu’il faut comparer une valeur avec sa valeur lors du dernier rendu.

import { useEffect, useRef } from "react";
function usePrevious(value) {
  const ref = useRef();  useEffect(() => {
    ref.current = value;
  }, [value]);  return ref.current;
}

Cela est particulièrement pratique pour les animations ou pour détecter quand quelque chose a réellement changé.

useClickOutside ferme les éléments UI tels que les modaux lorsque l’utilisateur clique à l’extérieur d’eux, ce qui correspond à la manière dont les utilisateurs s’attendent intuitivement à ce que ces composants se comportent.

import { useEffect } from "react";
function useClickOutside(ref, callback) {
  useEffect(() => {
    function handleClick(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        callback();
      }
    }    document.addEventListener("mousedown", handleClick);    return () =>
      document.removeEventListener("mousedown", handleClick);
  }, [ref, callback]);
}

Cela fonctionne bien pour les menus déroulants, les popovers et les menus de navigation mobile.

useDocumentTitle met à jour le titre de l’onglet du navigateur en fonction de la vue actuelle, ce qui facilite la navigation et l’orientation.

import { useEffect } from "react";
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

Plutôt que de dupliquer le même effet dans de nombreux composants, il suffit d’appeler ce hook chaque fois qu’une page a besoin d’un titre personnalisé.

useFetch encapsule la récupération de données de base dans un hook réutilisable.

import { useState, useEffect } from "react";
function useFetch(url) {
  const [data, setData] = useState(null);  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);  return data;
}

Pour tout projet plus complexe que de petits projets, des outils tels que React Query ou SWR sont généralement plus adaptés, mais cette version légère suffit pour lancer une petite application.

useCopyToClipboard gère le bouton de copie désormais omniprésent.

function useCopyToClipboard() {
  const copy = (text) => {
    navigator.clipboard.writeText(text);
  };
  return copy;
}

C’est une solution idéale pour partager des liens d’invitation, des codes de coupon ou des clés API avec un seul clic.

useOnlineStatus permet à votre application de réagir lorsque la connexion réseau est interrompue ou restaurée.

import { useState, useEffect } from "react";
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);  useEffect(() => {
    window.addEventListener("online", () => setOnline(true));
    window.addEventListener("offline", () => setOnline(false));
  }, []);  return online;
}

C’est une petite amélioration, mais elle améliore considérablement l’expérience utilisateur lorsque la connectivité est instable.

useDarkMode gère le passage en mode sombre que la plupart des utilisateurs attendent aujourd’hui des applications modernes.

import { useEffect } from "react";
function useDarkMode(enabled) {
  useEffect(() => {
    document.body.classList.toggle("dark", enabled);
  }, [enabled]);
}

Il est généralement utilisé en combinaison avec useLocalStorage afin que le mode choisi persiste entre les sessions.

Finalement, useTimeout rend le travail avec les délais beaucoup plus ordonné que d’éparpiller des appels bruts à setTimeout dans vos composants.

import { useEffect } from "react";
function useTimeout(callback, delay) {
  useEffect(() => {
    const timer = setTimeout(callback, delay);    return () => clearTimeout(timer);
  }, [callback, delay]);
}

Il convient aux notifications, aux écrans de démarrage et à toute action qui doit s’exécuter après un délai.

Dans de nombreux projets React, une leçon reste constamment valable : il n’est pas nécessaire de tout reconstruire à partir de zéro à chaque fois. Conserver une petite bibliothèque d’hooks réutilisables accélère le développement, rend les composants plus faciles à lire et élimine le code redondant. Aucun de ces douze hooks n’est particulièrement complexe, mais ensemble ils permettent d’économiser beaucoup de temps au cours d’un projet. À mesure que vos propres projets grandissent, vous accumulerez probablement une collection personnelle similaire — et ce qui compte, ce n’est pas le nombre total d’hooks que vous possédez, mais plutôt s’ils résolvent les problèmes auxquels vous êtes constamment confronté. Une petite utilité que vous écrivez aujourd’hui pour un projet devient souvent quelque chose à quoi vous avez recours dans tous les projets suivants.

Comprendre une bibliothèque personnelle de hooks est utile au quotidien, mais il est tout aussi important de maîtriser les mécanismes qui sous-tendent ces derniers — ce que résout chaque hook de base, quand l’utiliser et comment il se comporte lors des rendus. C’est sur cette base plus profonde que l’utilisation des hooks cesse d’être une simple copie-collage pour devenir une véritable compréhension.

Au temps des composants fonctionnels, les composants ne pouvaient pas conserver d’état ni exécuter de effets secondaires ; cette logique résidait donc dans des composants de classe disposant d’un objet state et de méthodes de cycle de vie :

class Counter extends React.Component {
  state = {
    count: 0
  };

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  render() {
    return (
      <button onClick={this.increment}>
        {this.state.count}
      </button>
    );
  }
}

Avec les hooks, le même composant devient simplement une fonction qui appelle directement useState :

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

La version fonctionnelle est généralement plus facile à lire et à réutiliser.

Deux règles régissent les hooks. Premièrement, appelez-les toujours au niveau le plus élevé — jamais à l’intérieur de conditions, de boucles ou de fonctions imbriquées. Évitez ceci :

if (isLoggedIn) {
  useEffect(() => {
    // ❌
  }, []);
}


for (...) {
  useState(0); // ❌
}

Préférez conserver l’appel du Hook sans condition et placer la logique conditionnelle par la suite :

function Component() {
  const [count, setCount] = useState(0);

  if (count > 10) {
    // Normal conditional logic is fine
  }

  return ...;
}

Cette règle existe parce que React suit l’ordre des appels pour suivre l’état, et non son nom. Avec deux appels à useState :

function Component() {
  const [count] = useState(0);
  const [name] = useState("Salim");

  return ...;
}

React les aligne en fonction de leur position :

Hook #1 → count
Hook #2 → name

Si le premier appel devient conditionnel :

if (condition) {
  useState(0);
}

useState("Salim");

l’ordre peut changer entre les rendus :

Render 1:
Hook #1 → count
Hook #2 → name

Render 2:
Hook #1 → name

Lorsque cela se produit, React ne peut plus associer l’état stocké au bon appel, donc l’ordre doit rester fixe à chaque rendu.

useState permet à un composant de se souvenir d’une valeur entre les rendus :

const [count, setCount] = useState(0);

Il retourne une paire :

count     → current state
setCount  → state update function

Un compteur simple illustre ce schéma :

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Clic sur le bouton déclenche cette séquence :

Click
 ↓
setCount()
 ↓
React schedules update
 ↓
Component renders again
 ↓
New state is calculated
 ↓
DOM is updated if necessary

Le state n’est pas une variable ordinaire comme let count = 0, car celle-ci ne survivrait pas à une nouvelle exécution de la fonction. React le stocke en dehors du composant et fournit la valeur appropriée à chaque rendu :

Render 1
count = 0

Render 2
count = 1

Render 3
count = 2

Le composant est exécuté à nouveau, mais React conserve le state entre les exécutions.

Cela est important lorsqu’on met à jour le state plusieurs fois dans un même gestionnaire d’événements :

setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

Si le rendu a commencé avec count égal à 0, les trois lignes utilisent ce même état, ce qui donne :

setCount(1)
setCount(1)
setCount(1)

au lieu de trois incréments réels. La solution consiste à utiliser une mise à jour fonctionnelle :

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

Désormais, React applique chaque mise à jour séquentiellement, car chaque fonction de mise à jour reçoit la valeur la plus récente — ce qui est utile lorsque le nouveau state dépend du state ancien.

useEffect synchronise un composant avec quelque chose en dehors de React :

useEffect(() => {
  // synchronization logic
}, [dependencies]);

Les cas typiques incluent les appels API, les temporisateurs, les écouteurs d’événements, WebSockets, d’autres API de navigateur, les abonnements et les bibliothèques tierces. Exemple :

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  return <h1>{user?.name}</h1>;
}

Cet effet est synchronisé avec userId ; lorsqu’il change, l’effet doit être exécuté à nouveau.

Ce comportement provient du tableau de dépendances. Étant donné :

useEffect(() => {
  console.log(count);
}, [count]);

React compare conceptuellement les dépendances :

Previous dependencies
        ↓
New dependencies
        ↓
Compare
        ↓
Changed?

S’ils diffèrent, l’effet est exécuté à nouveau :

Previous: [1]
New:      [2]

→ Effect needs to run

S’ils correspondent, il est sauté :

Previous: [2]
New:      [2]

→ Effect can be skipped

La comparaison suit Object.is, et non l’égalité profonde.

Les effets peuvent retourner une fonction de nettoyage, qui s’exécute avant le prochain effet et lors du démontage :

useEffect(() => {
  const timer = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

Le nettoyage est important pour des éléments tels que :

Timers
Event listeners
Subscriptions
WebSocket connections
Abortable requests

Lorsque les dépendances changent, React nettoie l’effet ancien avant d’exécuter le nouveau ; il effectue également ce nettoyage lorsque le composant est démonté.

Un champ de recherche illustre cela en pratique — chaque frappe peut déclencher une requête :

react
react hooks
react performance

Puisqu’une réponse obsolète pourrait écraser une plus récente, annulez la requête précédente :

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(setResults)
    .catch(error => {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    });

  return () => {
    controller.abort();
  };
}, [query]);

Le cycle de vie se présente comme suit :

query changes
     ↓
cleanup previous effect
     ↓
abort previous request
     ↓
start new request

Les champs de recherche réels combinent généralement cela avec le débouncing.

Une erreur courante consiste à utiliser useEffect pour des valeurs qui peuvent être dérivées directement :

const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Puisque fullName provient directement de valeurs existantes, omettez complètement l’effet et l’état :

const fullName = `${firstName} ${lastName}`;

La version basée sur un effet ajoute une synchronisation inutile ainsi qu’un rendu supplémentaire. Règle générale : si vous pouvez le calculer pendant le rendu, vous n’avez pas besoin d’un effet pour cela.

useRef fournit un objet stable dont la propriété .current reste inchangée entre les rendus, sans déclencher de nouveau rendu lorsque cette valeur change :

const ref = useRef(initialValue);

Une utilisation courante consiste à faire référence à un nœud DOM, par exemple pour mettre en surbrillance un champ de saisie :

function Input() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>
        Focus
      </button>
    </>
  );
}

useRef est également utile pour stocker des valeurs mutables qui ne doivent absolument pas figurer dans le rendu final, comme l’identifiant d’un chronomètre :

const timerRef = useRef(null);

On y assigne ensuite directement des valeurs, de la même manière que l’on le ferait pour n’importe quelle propriété d’objet :

timerRef.current = setInterval(...);

La modification

ref.current

n’entraîne pas à elle seule un nouveau rendu. Comparez ces deux modèles de pensée :

useState
→ update → render

useRef
→ mutate .current → no render

Cela fait des refs un outil adapté aux valeurs qui doivent persister entre les rendus sans influencer ce qui est affiché à l’écran.

useMemo adopte une approche différente : il met en cache le résultat d’un calcul plutôt qu’une référence DOM.

const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data]);

Pensez-y comme ceci : useMemo = mettre en cache un calcul. Un cas d’usage typique consiste à filtrer une liste uniquement lorsque les données de base changent réellement :

function ProductList({ products, search }) {
  const filteredProducts = useMemo(() => {
    return products.filter(product =>
      product.name
        .toLowerCase()
        .includes(search.toLowerCase())
    );
  }, [products, search]);

  return <ProductGrid products={filteredProducts} />;
}

Si une partie de l’état non liée est mise à jour, React peut simplement restituer la valeur déjà calculée tant que les dépendances restent identiques.

Conceptuellement, React conserve un petit enregistrement associé à l’appel du hook :

Hook
 ├── memoized value
 └── dependencies

Lors de la première rendu :

data = A

calculate(A)
 ↓
result = X

Lors d’un rendu ultérieur, si data reste égal à A, la comparaison des dépendances est positive et React réutilise X sans recalculer. Ce n’est que lorsque data devient quelque chose comme B que React effectue à nouveau le calcul.

Cela dit, useMemo est facile à surutiliser. Envelopper quelque chose comme ceci :

const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

est rarement justifié — le calcul est trivial, et la mémorisation elle-même n’est pas gratuite ; elle ajoute des surcoûts et du code supplémentaire à gérer. Recourir à useMemo uniquement lorsque le calcul est réellement coûteux, lorsqu’on a besoin d’une référence stable à un objet ou à un tableau, ou lorsque l’analyse des performances (ou une réflexion approfondie) montre qu’il est effectivement utile.

useCallback est l’outil équivalent pour les références de fonctions plutôt que leurs valeurs.

const handleClick = useCallback(() => {
  doSomething();
}, []);

Cela est important car les fonctions ordinaires sont recréées à chaque rendu :

function Parent() {
  const handleClick = () => {
    console.log("clicked");
  };

  return <Child onClick={handleClick} />;
}

Lors du premier rendu, handleClick fait référence à une instance de fonction ; lors du deuxième rendu, il fait référence à une autre instance, de sorte que les deux ne sont pas égaux même s’ils effectuent la même action.

Cette inégalité devient un problème lorsque un composant enfant est enveloppé dans React.memo:

const Child = React.memo(function Child({ onClick }) {
  console.log("Child rendered");

  return (
    <button onClick={onClick}>
      Click
    </button>
  );
});

et le composant parent ressemble à ceci :

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {count}
      </button>

      <Child onClick={handleClick} />
    </>
  );
}

Lorsque count change :

Parent renders
      ↓
handleClick reference remains stable
      ↓
React.memo checks Child props
      ↓
onClick unchanged
      ↓
Child can skip rendering

Sans useCallback, la référence de fonction nouvellement créée sera considérée comme « nouvelle » par le composant enfant mémorisé, ce qui le forcera à se rérender même si rien d’important n’a changé pour lui.

Néanmoins, useCallback n’est pas quelque chose à utiliser systématiquement pour chaque fonction. Avant d’envelopper un gestionnaire d’événement, demandez-vous si quelque chose bénéficie réellement d’une référence stable — par exemple :

React.memo child
Dependency array
Expensive downstream computation

Si aucune de ces conditions n’est remplie, il est généralement suffisant de laisser la fonction être recréée normalement.

La différence entre ces deux hooks réside dans ce qu’ils mémorisent :

useMemo
→ memoizes a VALUE

useCallback
→ memoizes a FUNCTION

Conceptually:

useMemo(() => calculateValue(), deps);

useCallback(() => doSomething(), deps);

En passant des hooks axés sur les performances à un modèle structuré, l’API Contexte vise à résoudre un problème différent : le transfert de données à travers de multiples niveaux de composants imbriqués. Sans elle, une valeur comme l’utilisateur actuel pourrait devoir parcourir chaque niveau intermédiaire :

App
 ↓
Navbar
 ↓
UserMenu
 ↓
Profile
 ↓
Avatar

ce qui donne quelque chose comme :

<App user={user} />
<Navbar user={user} />
<UserMenu user={user} />
<Profile user={user} />
<Avatar user={user} />

Ce schéma de transmission des props à travers des composants qui n’en ont pas besoin est connu sous le nom de prop drilling. La mise en place du contexte commence par une appel de création :

const UserContext = createContext(null);

Ensuite, on fournit une valeur à l’aide d’un fournisseur :

function App() {
  const user = {
    name: "Salim",
    role: "Developer"
  };

  return (
    <UserContext.Provider value={user}>
      <Dashboard />
    </UserContext.Provider>
  );
}

et on la lit là où elle est nécessaire :

function Profile() {
  const user = useContext(UserContext);

  return <h1>Hello {user.name}</h1>;
}

Profile n’a plus besoin que user lui soit transmis à travers chaque composant intermédiaire. Un cas courant dans la pratique est le thématisation :

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Dashboard />
    </ThemeContext.Provider>
  );
}

Tout composant peut alors lire directement le thème actuel :

function Button() {
  const { theme } = useContext(ThemeContext);

  return (
    <button className={theme}>
      Submit
    </button>
  );
}

ce qui produit un arbre ayant à peu près cette forme :

App
 │
 └── ThemeProvider
       │
       └── Dashboard
             │
             └── Button

Button récupère la valeur du thème sans avoir à descendre dans les propriétés hiérarchiques. Il convient de préciser que Context n’est pas automatiquement une solution complète de gestion d’état. Il répond à une question spécifique : comment rendre une valeur accessible aux composants situés profondément dans l’arbre ? Il ne fournit pas les mécanismes supplémentaires — sélecteurs, middleware, mises à jour structurées — que propose une bibliothèque dédiée. Pour des besoins d’état plus complexes, vous pourriez recourir à :

Redux
Zustand
Jotai
Reducer + Context

Context doit être considéré avant tout comme un mécanisme de distribution de valeurs, et non comme un gestionnaire d’état. Il a également des implications pour le rendu. Étant donné :

<ThemeContext.Provider value={theme}>

Lorsque la valeur de ce contexte change, les composants qui l’utilisent peuvent se rérender à nouveau. Le contexte ne prévient pas magiquement les rérenders supplémentaires — placer un objet volumineux et souvent modifié dans un contexte largement utilisé peut en fait entraîner des opérations inutiles. De meilleurs candidats pour un contexte sont des valeurs qui changent rarement, comme par exemple :

Theme
Locale
Authentication information
Feature flags
Application configuration

Les Custom Hooks vous permettent de regrouper une logique réutilisable construite à partir des hooks intégrés. Un exemple minimal :

function useCounter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(c => c + 1);
  }

  return {
    count,
    increment
  };
}

Utilisé de cette manière :

function Counter() {
  const { count, increment } = useCounter();

  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

La véritable valeur ici réside dans la réutilisation de la logique, et non dans la réutilisation du markup.

C’est important de le souligner : les Custom Hooks partagent la logique, mais pas l’état. Si deux composants distincts appellent chacun le même Custom Hook :

const counterA = useCounter();
const counterB = useCounter();

ils n’ont pas pour autant un seul count en commun. Chaque appel dispose de son propre état indépendant. Conceptuellement :

Component A
 └── useCounter
      └── useState → State A

Component B
 └── useCounter
      └── useState → State B

Un Hook personnalisé gère un comportement, et non un stockage de données partagé.

Pour donner un exemple concret, imaginez qu’une application doive afficher si l’utilisateur dispose actuellement d’une connexion réseau. Plutôt que de dupliquer la logique d’écouteur d’événements du navigateur dans chaque composant qui en a besoin, vous pouvez l’envelopper dans un Hook personnalisé :

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    navigator.onLine
  );

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return isOnline;
}

Tout composant ayant besoin de l’état de connexion peut désormais l’utiliser directement :

function Navbar() {
  const isOnline = useOnlineStatus();

  return (
    <span>
      {isOnline ? "🟢 Online" : "🔴 Offline"}
    </span>
  );
}

Un autre composant peut utiliser le même Hook pour modifier complètement sa propre affichage :

function Checkout() {
  const isOnline = useOnlineStatus();

  if (!isOnline) {
    return <p>You are offline.</p>;
  }

  return <PaymentForm />;
}

Un schéma similaire s’applique au filtrage des entrées en changement rapide :

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

utilisé à l’intérieur d’une fonction de recherche comme ceci :

function Search() {
  const [query, setQuery] = useState("");

  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (!debouncedQuery) return;

    // Search API
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
    />
  );
}

ce qui produit cette séquence d’événements :

User types
    ↓
query changes
    ↓
500ms wait
    ↓
debouncedQuery changes
    ↓
API request

Ces éléments de base sont conçus pour être combinés, et non utilisés isolément :

                 React Component
                       │
       ┌───────────────┼────────────────┐
       │               │                │
   useState        useEffect        useContext
       │               │                │
    UI state       External systems   Shared data
       │
       └───────────────┐
                       │
                 Custom Hook
                       │
             ┌─────────┼─────────┐
             ▼         ▼         ▼
         useState   useEffect   useMemo

Un hook personnalisé useProducts(), par exemple, pourrait dépendre en interne de :

useState
+
useEffect
+
useMemo

lorsqu’il récupère l’état d’authentification depuis useContext. Les re-render suivent un parcours prévisible :

State / Props / Context change
          ↓
      React update
          ↓
       Render
          ↓
   Reconciliation
          ↓
       Commit
          ↓
      DOM update
          ↓
      Browser paint

chaque hook ayant un rôle distinct, résumé ici :

useState
→ provides state + schedules updates

useMemo
→ calculates/reuses values during render

useCallback
→ calculates/reuses function references during render

useContext
→ reads context during render

useEffect
→ synchronizes with external systems after commit

Lectures complémentaires

  • React Components 101 : Créer des éléments UI réutilisables et faciles à maintenir — Découvrez pourquoi diviser l’interface utilisateur en petits composants React améliore la réutilisabilité, la lisibilité et la collaboration d’équipe, puis créez votre premier composant fonctionnel.
  • Pourquoi catch () génère un SyntaxError en JavaScript — Apprenez pourquoi une liste de paramètres vide dans catch perturbe complètement l’analyse du code JavaScript, et découvrez les deux manières grammaticalement correctes d’écrire un bloc catch sans paramètres.
  • Arrêter la synchronisation de l’état avec useEffect : un pattern React plus sécurisé — Découvrez pourquoi l’utilisation de useEffect pour synchroniser l’état dérivé provoque des conditions de course et des rendus supplémentaires, ainsi que la manière de le remplacer par une dérivation en temps de rendu et l’attribut key.