web-dev-qa-db-fra.com

Référencer un état obsolète dans React useEffect hook

Je veux enregistrer l'état dans localStorage lorsqu'un composant est démonté. Cela fonctionnait auparavant dans componentWillUnmount.

J'ai essayé de faire la même chose avec le crochet useEffect, mais il semble que l'état ne soit pas correct dans la fonction de retour de useEffect.

Pourquoi donc? Comment puis-je enregistrer l'état sans utiliser de classe?

Voici un exemple factice. Lorsque vous appuyez sur Fermer, le résultat est toujours 0.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Why is count in console always 0 ?</div>}
    </div>
  );
}

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

  useEffect(() => {
    // TODO: Load state from localStorage on mount

    return () => {
      console.log("count:", count);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector("#app"));

CodeSandbox

11
r03

J'ai essayé de faire la même chose avec le crochet useEffect, mais il semble que l'état ne soit pas correct dans la fonction de retour de useEffect.

La raison en est due aux fermetures. Une fermeture est une référence d'une fonction aux variables de son champ d'application. Votre callback useEffect n'est exécuté qu'une seule fois lorsque le composant est monté et donc le callback de retour fait référence à la valeur de comptage initiale de 0.

Les réponses données ici sont ce que je recommanderais. Je recommanderais la réponse de @Jed Richard de passer [count] à useEffect, ce qui a pour effet d'écrire dans localStorage uniquement lorsque le nombre change. C'est mieux que l'approche de ne rien passer du tout d'écriture à chaque mise à jour. À moins que vous ne changiez le nombre extrêmement fréquemment (toutes les quelques ms), vous ne verrez pas de problème de performances et il est correct d'écrire dans localStorage chaque fois que count change.

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

Si vous insistez pour n'écrire sur localStorage que lors du démontage, il y a un hack/solution laid que vous pouvez utiliser - refs. Fondamentalement, vous créez une variable qui est présente tout au long du cycle de vie du composant et que vous pouvez référencer de n'importe où à l'intérieur. Cependant, vous devrez synchroniser manuellement votre état avec cette valeur et c'est extrêmement gênant. Les références ne vous donnent pas le problème de fermeture mentionné ci-dessus car refs est un objet avec un champ current et plusieurs appels à useRef vous renverront le même objet. Tant que vous mutez le .current value, votre useEffect peut toujours (uniquement) lire la valeur la plus récente.

lien CodeSandbox

const {useState, useEffect, useRef} = React;

function Example() {
  const [tab, setTab] = useState(0);
  return (
    <div>
      {tab === 0 && <Content onClose={() => setTab(1)} />}
      {tab === 1 && <div>Count in console is not always 0</div>}
    </div>
  );
}

function Content(props) {
  const value = useRef(0);
  const [count, setCount] = useState(value.current);

  useEffect(() => {
    return () => {
      console.log('count:', value.current);
    };
  }, []);

  return (
    <div>
      <p>Day: {count}</p>
      <button
        onClick={() => {
          value.current -= 1;
          setCount(value.current);
        }}
      >
        -1
      </button>
      <button
        onClick={() => {
          value.current += 1;
          setCount(value.current);
        }}
      >
        +1
      </button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}

ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
8
Yangshun Tay

Votre fonction de rappel useEffect affiche le nombre initial, car votre useEffect n'est exécuté qu'une seule fois sur le rendu initial et le rappel est stocké avec la valeur de count qui était présente lors du rendu initial, qui est zéro.

Ce que vous feriez à la place dans votre cas, c'est

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  });

Dans les documents React, vous trouverez une raison pour laquelle il est défini comme ceci

Quand exactement React nettoie-t-il un effet? React effectue le nettoyage quand le composant se démonte. Cependant, comme nous l'avons appris précédemment, les effets s'exécutent pour chaque rendu et non pas une seule fois. C'est pourquoi React nettoie également les effets du rendu précédent avant de les exécuter la prochaine fois.

Lisez les documents de réaction sur Why Effects Run on Each Update

Il s'exécute sur chaque rendu, pour l'optimiser, vous pouvez le faire fonctionner sur count change. Mais il s'agit du comportement actuel proposé de useEffect, également mentionné dans la documentation, et peut changer dans l'implémentation réelle.

 useEffect(() => {
    // TODO: Load state from localStorage on mount
    return () => {
      console.log("count:", count);
    };
  }, [count]);
4
Shubham Khatri

L'autre réponse est correcte. Et pourquoi ne pas passer [count] à votre useEffect, et ainsi enregistrer dans localStorage chaque fois que count change? Il n'y a pas de véritable pénalité de performance en appelant localStorage comme ça.

2
Jed Richards

Essayez ce modèle:

function Content(props) {
  [count, setCount] = useState(0);

  // equivalent of componentWillUnmount:
  useEffect(() => () => {
    console.log('count:', count);
  }, []);

  // or to have a callback in place every time the state of count changes:
  useEffect(() => () => {
    console.log('count has changed:', count);
  }, [count]);

}

En d'autres termes, N'UTILISEZ PAS const/let/var, mais déclarez votre variable d'état et votre setter à la portée du composant (fonction). Cela évitera leur initialisation incorrecte.

Notez également la fonction légèrement plus supportable (à mon avis!) Qui renvoie une construction de code de fonction pour useEffect.

0
Andy Lorenz

Au lieu de suivre manuellement vos changements d'état comme dans la réponse acceptée, vous pouvez utiliser useEffect pour mettre à jour la référence.

function Content(props) {
  const [count, setCount] = useState(0);
  const currentCountRef = useRef(count);

  // update the ref if the counter changes
  useEffect(() => {
    currentCountRef.current = count;
  }, [count]);

  // use the ref on unmount
  useEffect(
    () => () => {
      console.log("count:", currentCountRef.current);
    },
    []
  );

  return (
    <div>
      <p>Day: {count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => props.onClose()}>close</button>
    </div>
  );
}
0
giggo1604