2 votes

La fermeture sur le résultat d'un hook `useState` provoque un comportement gênant.

J'ai un composant comme celui-ci :

const MyInput = ({ defaultValue, id, onChange }) => {

  const [value, setValue] = useState(defaultValue);
  const handleChange = (e) => {
    const value = e.target.value;
    setValue(value);

    onChange(id, value);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
};

Je veux qu'il se comporte comme un composant non contrôlé, mais je veux qu'il ait une valeur initiale que nous pouvons définir.

Dans mon composant parent, cela fonctionne bien,

export default function App() {
  const [formState, setFormState] = useState({});

  const handleChange = (id, value) => {
    console.log(id, value, JSON.stringify(formState)); // For debugging later
    setFormState({ ...formState, [id]: value });
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}

mais le problème est que la valeur de formState ne reflète pas les valeurs sur le MyInputs jusqu'à ce qu'ils déclenchent un événement onchange.

Donc ok, je peux juste ajouter un hook useEffect 'component did mount' sur MyInput pour déclencher un onChange lorsque le composant se monte pour la première fois.

const MyInput = ({ defaultValue, id, onChange }) => {
  useEffect(() => {
    onChange(id, defaultValue);
  }, []);

  const [value, setValue] = useState(defaultValue);
  const handleChange = (e) => {
    const value = e.target.value;
    setValue(value);

    onChange(id, value);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
};

Sandbox de code

Mais cela ne se comporte pas comme nous le souhaitons. Chacune des MyInputs appelle onChange lorsqu'elle est montée pour la première fois, mais la référence à formState est périmé pour le gestionnaire principal onChange dans chaque cas.

Par exemple, la sortie du journal de la console lors du premier chargement de la page est la suivante :

1 one {}
2 two {}
3 three {}
4 four {}
5 five {}

J'ai donc pensé que l'utilisation d'une référence pourrait résoudre ce problème. par exemple :

export default function App() {
  const [formState, setFormState] = useState({});

  const formStateRef = useRef(formState);

  useEffect(() => {
    formStateRef.current = formState;
  }, [formState]);

  const handleChange = (id, value) => {
    console.log(
      id,
      value,
      JSON.stringify(formState),
      JSON.stringify(formStateRef)
    );
    setFormState({ ...formStateRef.current, [id]: value });
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}

Code Sandbox

Ça ne marche pas. Il semble que tous les gestionnaires de changement déclenchent avant useEffect sur formState a la possibilité de mettre à jour la ref.

1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}

Je suppose que je pourrais directement muter la référence dans la fonction handleChange, et la définir ?

export default function App() {
  const [formState, setFormState] = useState({});

  const formStateRef = useRef(formState);

  const handleChange = (id, value) => {
    console.log(
      id,
      value,
      JSON.stringify(formState),
      JSON.stringify(formStateRef)
    );

    formStateRef.current = { ...formStateRef.current, [id]: value };
    setFormState(formStateRef.current);
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}

Code Sandbox

Ça marche. Mais cette utilisation d'arbitres me met un peu mal à l'aise.

Quelle est la méthode standard pour réaliser quelque chose comme ça ?

3voto

Drew Reese Points 1957

Si vous prenez votre premier exemple et que vous utilisez simplement une mise à jour de l'état fonctionnel à la place, je pense que vous obtiendrez ce que vous recherchez.

Numéro

Avec la mise à jour non fonctionnelle, toutes les entrées se montent en même temps et invoquent toutes leur onChange manipulateurs. Le site handleChange utilise l'état du cycle de rendu dans lequel il était inclus. Lorsque toutes les entrées mettent en file d'attente les mises à jour dans le même cycle de rendu, chaque mise à jour suivante écrase la mise à jour d'état précédente. La dernière entrée à mettre à jour l'état est celle qui "gagne". C'est pourquoi vous voyez :

{
  "5": "five"
}

Au lieu de

{
  "1": "one",
  "2": "two",
  "3": "three",
  "4": "four",
  "5": "five"
}

Solution

Utilisez une mise à jour d'état fonctionnelle pour mettre à jour correctement à partir de l'état précédent, et non de l'état du cycle de rendu précédent.

setFormState(formState => ({ ...formState, [id]: value }));

A partir de là, les suggestions ne sont que des optimisations

  1. En conservant l'identifiant de l'entrée dans le gestionnaire, cela fait un props de moins à passer et à traiter. Les entrées enfants n'ont pas non plus besoin de le connaître pour gérer les changements.

    const handleChange = (id) => (value) => {
      setFormState((formState) => ({ ...formState, [id]: value }));
    };
  2. Passez defaultValue au defaultValue prop des entrées sous-jacentes.

    const MyInput = ({ defaultValue, onChange }) => {
      useEffect(() => {
        onChange(defaultValue);
      }, []);
    
      const handleChange = (e) => {
        const { value } = e.target;
        onChange(value);
      };
    
      return (
        <div>
          <input type="text" defaultValue={defaultValue} onChange={handleChange} />
        </div>
      );
    };
  3. Passez le id dans le gestionnaire de curry

    <MyInput defaultValue="one" onChange={handleChange(1)} />
    <MyInput defaultValue="two" onChange={handleChange(2)} />
    <MyInput defaultValue="three" onChange={handleChange(3)} />
    <MyInput defaultValue="four" onChange={handleChange(4)} />
    <MyInput defaultValue="five" onChange={handleChange(5)} />

Démo

Edit closure-on-the-result-of-a-usestate-hook-causes-annoying-behaviour

code de démonstration

const MyInput = ({ defaultValue, onChange }) => {
  useEffect(() => {
    onChange(defaultValue);
  }, []);

  const handleChange = (e) => {
    const { value } = e.target;
    onChange(value);
  };

  return (
    <div>
      <input type="text" defaultValue={defaultValue} onChange={handleChange} />
    </div>
  );
};

export default function App() {
  const [formState, setFormState] = useState({});

  const handleChange = (id) => (value) => {
    setFormState((formState) => ({ ...formState, [id]: value }));
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput defaultValue="one" onChange={handleChange(1)} />
      <MyInput defaultValue="two" onChange={handleChange(2)} />
      <MyInput defaultValue="three" onChange={handleChange(3)} />
      <MyInput defaultValue="four" onChange={handleChange(4)} />
      <MyInput defaultValue="five" onChange={handleChange(5)} />
    </div>
  );
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X