Codelynx.dev
Posts

Quand utiliser useEffect - Le guide complet

09/10/2024 Melvynx

useEffect est très souvent mal utilisé et mal documenté sur internet. Aujourd'hui je vais t'expliquer précisément quand et pourquoi il faut l'utiliser. Je vais te faire comprendre une bonne fois pour toutes à quoi sert le useEffect.

(spoiler, il ne doit généralement pas être utilisé pour faire des fetch, il y a bien mieux à faire.)

Quelle est l'utilité du useEffect ?

Le useEffect a comme rôle de venir synchroniser ton composant avec des éléments externes à React. Un bon autre nom pour le useEffect aurait été useSideEffect, il permet de gérer les effets de bord de ton composant.

C'est quoi un side effect ?

Un side effect ou effet de bord est une action qui doit être effectuée en dehors de React. React gère beaucoup de choses, comme les événements DOM, les states, le rendu de ton application, mais il y a certaines choses qu'il ne gère pas.

Le useEffect permet de synchroniser ton component avec des éléments externes.

Je pourrais te citer quelques exemples de synchronisation :

  • Des Web Sockets
  • Le DOM avec des event listeners
  • Des Backends Externes
  • Des Librairies externes

On peut faire un exemple pour un composant qui se synchroniserait avec la position de la souris :

import { useEffect, useState } from "react";

export default function App() {
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      setMousePosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  return (
    <div>
      Mouse Position: {mousePosition.x}, {mousePosition.y}
    </div>
  );
}

Tu peux voir qu'ici, on vient utiliser le useEffect pour venir écouter les événements de document pour mettre à jour notre state.

Pourquoi synchroniser ?

Il faut bien comprendre comment est constitué un useEffect :

JSX
useEffect(
  () => {
    // 1. Run Effect

    return () => {
      // 2. CleanUp Effect
    };
  },
  [
    /* 3. Dependencies */
  ]
);

Il a vraiment ces 3 parties qui fonctionnent ensemble.

  1. Run Effect : C'est la partie qui va être exécutée à chaque fois que le component est monté ou que les dépendances changent.
  2. CleanUp Effect : C'est la partie qui va être exécutée à chaque fois que le component est démonté.
  3. Dependencies : C'est la partie qui va permettre de dire à React quand il doit exécuter le Run Effect et le CleanUp Effect.

S'il y a cleanup effect c'est qu'aucune synchronisation ne vient sans la présence d'un cleanup qui répond à la question :

Que faire quand notre composant n'est PLUS synchronisé ?

C'est justement le rôle que va avoir notre CleanUp Effect.

En règle générale, chaque effet doit être accompagné d'un CleanUp Effect qui fait qu'on cleanup notre effect.

Quand le cleanup est-il appelé ?

Il va y avoir vraiment 3 phases différentes pour notre useEffect :

  1. Mount : C'est la première fois que le composant est monté. On va exécuter le useEffect dans tous les cas
  2. Update : C'est quand le composant est mis à jour. On va exécuter le useEffect seulement si les dépendances changent
  3. Unmount : C'est quand le composant est démonté. On va exécuter le Cleanup seulement si le composant est démonté

Le Unmount, tu le comprends, c'est quand notre composant n'est plus affiché dans le DOM.

L'update par contre, c'est quand notre effet est mis à jour via le tableau de dépendances, celui-ci va venir appeler le cleanup effect puis l'effet suivant dans cet ordre !

import React, { useState, useEffect } from "react";

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

  console.log(`Render : ${count}`);

  useEffect(() => {
    console.log(`Run Effect : ${count}`);

    return () => {
      console.log(`Cleanup Effect : ${count}`);
    };
  }, [count]);

  useEffect(() => {
    console.log(`Run Effect : Mount`);

    return () => {
      console.log(`Cleanup Effect : Unmount`);
    };
  }, []);

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default function App() {
  const [open, setOpen] = useState(true);
  return (
    <div className="App">
      <input
        type="checkbox"
        checked={open}
        onChange={(e) => setOpen(e.target.checked)}
      />
      {open ? <SimpleCounter /> : null}
    </div>
  );
}

Tu peux voir dans ce playground que dans l'ordre :

  1. Le Render est appelé
  2. Le Clean Up est appelé
  3. Le Run Effect est appelé

C'est toujours le dernier cleanup qui est appelé, tu peux t'en rendre compte car je log toujours le compteur.

Tu vas avoir ce genre de log :

TXT
Render : 2
Cleanup Effect : 1
Run Effect : 2

On commence par rendre le composant avec le compteur à 2, puis on va appeler le Cleanup Effect pour l'état précédent, donc le 1 et ensuite on appelle le Run Effect qui va être exécuté avec la dernière version du compteur, donc 2.

useEffect pour FETCH des données

J'ai dit au début de l'article que le useEffect n'était pas fait pour fetch des données, c'est partiellement faux.

Dans un monde où aucune librairie n'existe, useEffect est bien fait pour fetch des données.

Le problème c'est qu'il n'est pas optimisé et il peut créer des erreurs. La majorité des gens font ce genre de code :

import React, { useState, useEffect } from 'react';

const FetchAgeFromName = ({ name }) => {
const [age, setAge] = useState(null);

useEffect(() => {
const fetchAge = async () => {
try {
console.log("Fetch age");
const response = await fetch(`https://api.agify.io?name=${name}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
setAge(data.age);
} catch (e) {
console.error("Error");
}
};

  fetchAge();

}, [name]);

if (!age) return <p>Loading...</p>;

return <p>The estimated age for the name {name} is {age} years old.</p>;
};

export default function App() {
return (
  <div>
    <h1>Age Estimation Playground</h1>
    <FetchAgeFromName name="John" />
  </div>
);
}

Ce genre de code fonctionne, mais il n'est pas optimisé... pourquoi ? Car il n'y a pas de cleanup. Or chaque side effect doit avoir un cleanup.

De plus, on ne gère pas l'état d'erreur ou de chargement. Voici un code "propre" :

import React, { useState, useEffect } from 'react';

const FetchAgeFromName = ({ name }) => {
const [age, setAge] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

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

  const fetchAge = async () => {
    try {
      setIsLoading(true);
      const response = await fetch(`https://api.agify.io?name=${name}`);
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      const data = await response.json();
      setAge(data.age);
    } catch (error) {
      setHasError(true);
    } finally {
      setIsLoading(false);
    }
  };

  fetchAge();

  return () => {
    abortController.abort();
  };

}, [name]);

if (isLoading) return <p>Loading...</p>;
if (hasError) return <p>Error fetching age data.</p>;

return <p>The estimated age for the name {name} is {age} years old.</p>;
};

export default function App() {
return (
  <div>
    <h1>Age Estimation Playground</h1>
    <FetchAgeFromName name="John" />
  </div>
);
}

Ici on a tous les éléments qu'il faut :

  • Un AbortController qui va permettre de stopper le fetch
  • Un état de chargement qui va permettre de faire un loading
  • Un état d'erreur qui va permettre de faire un message d'erreur

Mais tu vois que le code est chiant et long ! Alors qu'avec SWR on peut faire :

import React, { useState, useEffect } from 'react';
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

const FetchAgeFromName = ({ name }) => {
const { data, error } = useSWR(`https://api.agify.io?name=${name}`, fetcher);

if (error) return <p>Error fetching age data.</p>;
if (!data) return <p>Loading...</p>;

return <p>The estimated age for the name {name} is {data.age} years old.</p>;
};

export default function App() {
return (
  <div>
    <h1>Age Estimation Playground</h1>
    <FetchAgeFromName name="John" />
  </div>
);
}

Tu te rends compte de la différence de code ? Il y a littéralement 3x moins de code, alors je te demande : pourquoi utiliser le useEffect ? dans ce cas ?

Quand NE PAS utiliser le useEffect

Il ne faut surtout PAS utiliser le useEffect pour venir gérer des choses qu'on pourrait gérer avec des events. Regarde ce code :

JSX
const CounterButton = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  useEffect(() => {
    notifyAnalytics(count);
  }, [count]);

  return (
    <div>
      <button onClick={() => incrementCount()}>Increment</button>
      <p>Current Count: {count}</p>
    </div>
  );
};

Ici le useEffect est utilisé comme une sorte de "quand count change, appel cette méthode" mais c'est le pire moyen de faire ça ! Ici au lieu d'avoir un "side effect" on veut juste un "fire event" :

JSX
const CounterButton = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    const newCount = count + 1;
    setCount(newCount);
    notifyAnalytics(newCount);
  };

  return (
    <div>
      <button onClick={() => incrementCount()}>Increment</button>
      <p>Current Count: {count}</p>
    </div>
  );
};

Et voilà, on a le même code mais ici on n'a pas besoin d'un useEffect.

Autre exemple, c'est celui de venir "écouter" les props d'un composant :

JSX
const CounterButton = ({ lastName }) => {
  const [fullName, setFullName] = useState(`Melvyn + ${lastName}`);

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

  return <div>{fullName}</div>;
};

Quand tu fais ça, c'est un peu inutile déjà car comme tu le vois, on n'utilise jamais setFullName. Ce qu'il te faut c'est faire un derived state :

JSX
const CounterButton = ({ lastName }) => {
  const fullName = `Melvyn + ${lastName}`;

  return <div>{fullName}</div>;
};

ici on a le même résultat sans avoir besoin d'un useEffect en plus.

Conclusion, quand utiliser le useEffect ?

Pour conclure, le useEffect est très utile pour "s'abonner" à des événements du DOM, comme on l'a vu un hook useMousePosition fait un très bon usage du useEffect :

JSX
export const useMousePosition = () => {
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      setMousePosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  return mousePosition;
};

Si tu veux t'aider pour ta décision tu peux te répondre à ces questions, si la réponse est toujours "oui" alors tu peux utiliser le useEffect.

QuestionRéponse
Ce useEffect a-t-il besoin d'un "cleanup" ?-
Est-ce que le useEffect ne PEUT PAS être remplacé par un simple "fire event" ?-
Est-ce que mon useEffect est bien en train de synchroniser avec un élément en dehors de React ?-

Voilà, si ça t'a plu tu devrais vraiment réfléchir à te former en React avec moi via mes cours gratuits :

Reçois une formation React gratuite
Deviens un expert React en comprenant tous les concepts avancés de cette librairie.

Du même style, je t'invite à aller regarder comment utiliser useRef, les formulaires en React ou le Guide de flexbox en CSS qui pourrait vraiment t'intéresser.