Résoudre l'erreur Can't perform a React state update on an unmounted component

Voir la vidéo
Description Sommaire

Dans ce tutoriel je vous propose de découvrir comment gérer une erreur assez classique de React

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.

La cause

Comme indiqué par l'erreur ce problème survient lorsque un callback qui change l'état d'un composant est appelé après que le composant soit démonté. Cela arrive souvent lorsque l'on contacte une API distante ou lorsque l'on oublie de clearTimeout() un intervalle précédemment créé.

const loadPosts = async () => {
  setLoading(true);
  try {
    const  response  =  await  fetch(
      "https://jsonplaceholder.typicode.com/posts?_limit=10"
    ).then((r) =>  r.json());
    // Ce code peut être éxécuté alors que le composant a été démonté :(
    setPosts(response);
    setLoading(false);
  } catch (e) {
    if (!(e  instanceof  DOMException) ||  e.code  !==  e.ABORT_ERR) {
      console.error(e);
    }
  }
};

La solution "pas top mais pas le choix"

La première solution consiste simplement à éviter le problème pour ne pas déclencher l'erreur. Cela revient à traiter le symptôme plus que le problème mais peut servir dans le cas où le code n'est pas annulable.

Le principe est d'utiliser une ref pour suivre l'état du composant et de vérifier l'état avant d'exécuter les changements d'états. On peut ainsi se créer un hook personnalisé pour automatiser ce processus.

export function useSafeState(initialValue = null) {
  // On crée un ref pour savoir si le composant est monté ou pas
  const isMounted = useRef(true);
  // Notre état que l'on va décoré
  const [state, setState] = useState(initialValue);
  // Quand le composant est démonté on change la valeur de la ref
  useEffect(() => () => (isMounted.current = false), []);

  // On décore le setState pour ne pas l'éxécuter si le composant est démonté
  const setStateSafe = useCallback((value) => {
    if (isMounted.current) {
      setState(value);
    }
  }, []);

  return [state, setStateSafe];
}

Maintenant vous pouvez remplacer le états problématique avec ce nouveau hook tout en sachant que vous ne résolvez pas le problème, vous l'ignorez.

La bonne solution

Dans notre cas la bonne solution est d'utiliser un AbortController afin d'annuler la promesse dans le cas où le composant est démonté. Encore une fois vous pouvez utiliser un hook personnalisé pour éviter au maximum la duplication.

// Exporte une fonction fetch qui s'auto annule lors du démontage
export function useFetch() {
  const abortController = useRef(new AbortController());
  useEffect(() => () => abortController.current.abort(), []);

  return useCallback(
    (url, options) =>
      fetch(url, { signal: abortController.current.signal, ...options }),
    []
  );
}

Maintenant vous pouvez utiliser votre fetch annulable dans votre code sans avoir à vous soucier du démontage de vos composants.

const fetch = useFetch();
const loadPosts = async () => {
  setLoading(true);
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts?_limit=10"
    ).then((r) => r.json());
    setPosts(response);
    setLoading(false);
  } catch (e) {
    if (!(e instanceof DOMException) || e.code !== e.ABORT_ERR) {
      console.error(e);
    }
  }
};

Vous pouvez aussi vous créer des hooks personnalisé pour avoir le même comportement pour des timers par exemple pour décorer setTimeout() et setInterval().

Publié
Technologies utilisées
Auteur :
Grafikart
Partager