Contactez-nous

`useEffect` avec dépendances spécifiques

Découvrez comment utiliser useEffect avec un tableau de dépendances spécifiques en React pour synchroniser vos effets de bord avec les changements de props ou d'état.

Synchroniser les effets avec les changements de données

Nous avons vu que `useEffect` sans dépendances s'exécute après chaque rendu, et avec des dépendances vides (`[]`) une seule fois au montage/démontage. Cependant, le cas d'usage le plus puissant et le plus fréquent de `useEffect` est de l'utiliser avec un tableau de dépendances spécifiques non vide. Ce pattern permet de lier l'exécution d'un effet de bord directement aux changements de certaines valeurs précises (props, état, ou autres valeurs issues de hooks).

L'idée fondamentale est la suivante : l'effet doit être ré-exécuté si, et seulement si, l'une des données dont il dépend pour son exécution a changé depuis la dernière fois. Cela garantit que l'effet reste synchronisé avec l'état actuel de l'application et évite des exécutions inutiles lorsque des parties non liées du composant sont mises à jour.

Syntaxe et mécanisme de comparaison

La syntaxe consiste à passer un tableau contenant les variables dont l'effet dépend comme second argument de `useEffect` :

const [valeurA, setValeurA] = useState(0);
const { propB } = props;

useEffect(() => {
  // Ce code utilise valeurA et propB
  console.log(`Exécution de l'effet avec A=${valeurA} et B=${propB}`);
  // ... faire quelque chose basé sur valeurA et propB ...

  return () => {
    // Nettoyage éventuel basé sur les valeurs précédentes de A et B
    console.log('Nettoyage avant prochain effet ou démontage');
  };
}, [valeurA, propB]); // <-- Dépendances spécifiques

Le mécanisme appliqué par React est le suivant :

  1. Premier Rendu : L'effet s'exécute toujours. React stocke les valeurs actuelles de `valeurA` et `propB`.
  2. Rendus Suivants : Avant d'exécuter l'effet, React compare chaque valeur dans le tableau de dépendances (`[nouvelleValeurA, nouvellePropB]`) avec la valeur correspondante stockée lors du rendu précédent (`[ancienneValeurA, anciennePropB]`). La comparaison utilise l'algorithme `Object.is`.
  3. Si aucune dépendance n'a changé : React saute l'exécution de l'effet (et de son nettoyage associé, sauf au démontage).
  4. Si au moins une dépendance a changé : React exécute d'abord la fonction de nettoyage de l'exécution *précédente*. Ensuite, il exécute la fonction principale de l'effet. Enfin, il stocke les nouvelles valeurs de `valeurA` et `propB` pour la prochaine comparaison.

Exemples d'utilisation

1. Data fetching basé sur une prop ou un état :

C'est un cas très fréquent. Si l'URL ou les paramètres de votre requête API dépendent d'une prop (comme un ID) ou d'un état (comme un terme de recherche), vous devez inclure cette valeur dans les dépendances.

function SearchResults({ query }) { // 'query' est une prop
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!query) { // Ne rien faire si la requête est vide
      setResults([]);
      return;
    }

    setIsLoading(true);
    const controller = new AbortController();

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

    // Nettoyage : annuler la requête si 'query' change avant la fin
    return () => controller.abort();

  }, [query]); // <-- L'effet se relance si 'query' change

  // ... Afficher isLoading ou results
}
2. Mise à jour d'un abonnement basé sur une prop :

Si vous vous abonnez à une source de données spécifique (par exemple, un chat room dont l'ID est une prop), vous devez nettoyer l'ancien abonnement et en créer un nouveau lorsque l'ID change.

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    console.log(`Connexion au chat room: ${roomId}`);
    const connection = chatAPI.subscribeToRoom(roomId, (newMessage) => {
      setMessages(prev => [...prev, newMessage]);
    });

    // Nettoyage: se déconnecter de la room précédente
    return () => {
      console.log(`Déconnexion du chat room: ${roomId}`);
      connection.unsubscribe();
    };
  }, [roomId]); // <-- Re-abonne/désabonne si roomId change

  // ... Afficher les messages
}
3. Réagir aux changements d'état pour des calculs ou effets dérivés :
function FormValidator({ value }) {
  const [isValid, setIsValid] = useState(true);

  useEffect(() => {
    // Simule une validation coûteuse ou asynchrone
    console.log(`Validation de la valeur: ${value}`);
    const validationTimeout = setTimeout(() => {
      setIsValid(value.length >= 5); // Exemple de règle
      console.log('Validation terminée.');
    }, 500);

    // Nettoyage: annule la validation précédente si la valeur change rapidement
    return () => clearTimeout(validationTimeout);

  }, [value]); // <-- Relance la validation si 'value' change

  return 
Valeur valide: {isValid ? 'Oui' : 'Non'}
; }

La règle des dépendances exhaustives et pièges courants

Comme mentionné précédemment, la règle la plus critique est d'inclure toutes les valeurs réactives (props, state, fonctions définies dans le composant, valeurs issues d'autres hooks) utilisées par l'effet dans le tableau de dépendances. Omettre une dépendance est la source la plus fréquente de bugs avec `useEffect`, menant souvent à des closures obsolètes (l'effet utilise une ancienne valeur de la variable oubliée).

Pièges courants liés aux types de dépendances :

  • Fonctions : Si vous utilisez une fonction définie à l'intérieur de votre composant dans `useEffect`, elle doit être incluse dans les dépendances. Or, une fonction définie classiquement (`function maFonction() {...}` ou `const maFonction = () => {...}`) obtient une nouvelle référence à chaque rendu, ce qui déclencherait l'effet à chaque fois ! Solutions :
    • Déplacer la fonction à l'intérieur de `useEffect` si elle n'est utilisée que là.
    • Déplacer la fonction à l'extérieur du composant si elle ne dépend d'aucun props ou state.
    • L'envelopper dans `useCallback` pour obtenir une version mémorisée qui ne change que si ses propres dépendances changent : `const maFonctionMemo = useCallback(() => { /* utilise propA */ }, [propA]);`. Ajoutez ensuite `maFonctionMemo` aux dépendances de `useEffect`.
  • Objets et Tableaux : Les objets (`{}`) ou tableaux (`[]`) créés directement dans le rendu obtiennent une nouvelle référence à chaque fois. Si vous les mettez en dépendance, ils déclencheront l'effet à chaque rendu. Solutions :
    • Dépendre de valeurs primitives si possible (ex: `[user.id]` au lieu de `[user]`).
    • Si l'objet/tableau est un état, React gère sa référence.
    • Si l'objet/tableau est dérivé ou une prop complexe, envisagez de le mémoriser avec `useMemo` avant de le passer en dépendance : `const optionsMemo = useMemo(() => ({ theme, layout }), [theme, layout]); useEffect(..., [optionsMemo]);`.

Le linter (`eslint-plugin-react-hooks` et sa règle `exhaustive-deps`) est indispensable pour vous aider à identifier et corriger ces problèmes.

Conclusion : L'outil clé pour la synchronisation

`useEffect` avec des dépendances spécifiques est le mécanisme central de React Hooks pour synchroniser les effets de bord avec l'état et les props du composant. En déclarant explicitement les données dont votre effet dépend, vous permettez à React d'optimiser l'exécution et de garantir que votre logique externe (API calls, abonnements, etc.) reste cohérente avec les données affichées.

Maîtriser le tableau de dépendances, comprendre comment React effectue les comparaisons, et respecter la règle des dépendances exhaustives sont des compétences essentielles pour exploiter pleinement la puissance de `useEffect` et construire des applications React fiables et performantes.