web-dev-qa-db-fra.com

Wrong React accroche le comportement avec l'écouteur d'événement

Je m'amuse avec React raccroche et fait face à un problème. Il indique le mauvais état lorsque j'essaie de le consigner à l'aide d'un bouton géré par un écouteur d'événement.

CodeSandbox: https://codesandbox.io/s/lrxw1wr97m

  1. Cliquez sur le bouton 'Ajouter une carte' 2 fois
  2. Dans la première carte, cliquez sur Button1 et voyez dans la console qu'il y a 2 cartes en état (comportement correct)
  3. Dans la première carte, cliquez sur Button2 (géré par l'écouteur d'événement) et voyez dans la console qu'il n'y a qu'une seule carte dans l'état (comportement incorrect)

Pourquoi montre-t-il le mauvais état? Dans la première carte, Button2 devrait afficher 2 cartes dans la console. Des idées?

import React, { useState, useContext, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const CardsContext = React.createContext();

const CardsProvider = props => {
  const [cards, setCards] = useState([]);

  const addCard = () => {
    const id = cards.length;
    setCards([...cards, { id: id, json: {} }]);
  };

  const handleCardClick = id => console.log(cards);
  const handleButtonClick = id => console.log(cards);

  return (
    <CardsContext.Provider
      value={{ cards, addCard, handleCardClick, handleButtonClick }}
    >
      {props.children}
    </CardsContext.Provider>
  );
};

function App() {
  const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
    CardsContext
  );

  return (
    <div className="App">
      <button onClick={addCard}>Add card</button>
      {cards.map((card, index) => (
        <Card
          key={card.id}
          id={card.id}
          handleCardClick={() => handleCardClick(card.id)}
          handleButtonClick={() => handleButtonClick(card.id)}
        />
      ))}
    </div>
  );
}

function Card(props) {
  const ref = useRef();

  useEffect(() => {
    ref.current.addEventListener("click", props.handleCardClick);
    return () => {
      ref.current.removeEventListener("click", props.handleCardClick);
    };
  }, []);

  return (
    <div className="card">
      Card {props.id}
      <div>
        <button onClick={props.handleButtonClick}>Button1</button>
        <button ref={node => (ref.current = node)}>Button2</button>
      </div>
    </div>
  );
}

ReactDOM.render(
  <CardsProvider>
    <App />
  </CardsProvider>,
  document.getElementById("root")
);

J'utilise React 16.7.0-alpha.0 et Chrome 70.0.3538.110

En passant, si je réécris CardsProvider en utilisant class, le problème a disparu. CodeSandbox utilisant la classe: https://codesandbox.io/s/w2nn3mq9vl

14
Mark Lano

C'est un problème courant pour les composants fonctionnels qui utilisent useState hook. Les mêmes préoccupations s’appliquent à toutes les fonctions de rappel où l’état useState est utilisé, par exemple. setTimeout ou setInterval fonctions de minuterie .

Les gestionnaires d'événements sont traités différemment dans les composants CardsProvider et Card.

handleCardClick et handleButtonClick utilisés dans le composant fonctionnel CardsProvider sont définis dans son étendue. Il y a de nouvelles fonctions à chaque exécution, elles font référence à l'état cards obtenu au moment où elles ont été définies. Les gestionnaires d’événements sont ré-enregistrés chaque fois que le composant CardsProvider est rendu.

handleCardClick utilisé dans Card le composant fonctionnel est reçu comme accessoire et enregistré une fois sur le montage du composant avec useEffect. Il s'agit de la même fonction pendant toute la durée de vie du composant et fait référence à l'état vicié qui était à l'état neuf à l'époque où la fonction handleCardClick a été définie pour la première fois. handleButtonClick est reçu comme accessoire et réenregistré à chaque rendu Card, c'est une nouvelle fonction à chaque fois et fait référence à l'état frais.

État mutable

Une approche courante pour résoudre ce problème consiste à utiliser useRef au lieu de useState. Une référence est une recette qui fournit un objet modifiable qui peut être transmis par référence:

const ref = useRef(0);

function eventListener() {
  ref.current++;
}

Si un composant doit être rendu à nouveau lors de la mise à jour de l'état, comme cela est attendu de useState, les références ne sont pas applicables.

Il est possible de conserver les mises à jour d'état et l'état mutable séparément, mais forceUpdate est considéré comme un antipattern dans les composants de classe et de fonction (répertoriés à titre de référence seulement):

const useForceUpdate = () => {
  const [, setState] = useState();
  return () => setState({});
}

const ref = useRef(0);
const forceUpdate = useForceUpdate();

function eventListener() {
  ref.current++;
  forceUpdate();
}

Fonction de mise à jour d'état

Une solution consiste à utiliser la fonction de programme de mise à jour d'état qui reçoit le nouvel état au lieu de l'état obsolète de la portée englobante:

function eventListener() {
  // doesn't matter how often the listener is registered
  setState(freshState => freshState + 1);
}

Dans le cas où un état est nécessaire pour un effet secondaire synchrone tel que console.log, une solution consiste à renvoyer le même état pour empêcher une mise à jour.

function eventListener() {
  setState(freshState => {
    console.log(freshState);
    return freshState;
  });
}

useEffect(() => {
  // register eventListener once
}, []);

Cela ne fonctionne pas bien avec les effets secondaires asynchrones, notamment les fonctions async.

Réenregistrement manuel des écouteurs d'événements

Une autre solution consiste à ré-enregistrer l'écouteur d'événements à chaque fois. Ainsi, un rappel reçoit toujours un nouvel état à partir de la portée englobante:

function eventListener() {
  console.log(state);
}

useEffect(() => {
  // register eventListener on each state update
}, [state]);

Gestion d'événements intégrée

À moins que l'écouteur d'événements ne soit enregistré sur document, window ou que d'autres cibles d'événements soient en dehors de la portée du composant actuel, la gestion des événements DOM propres à React doit être utilisée dans la mesure du possible. useEffect:

<button onClick={eventListener} />

Dans le dernier cas, l'écouteur d'événements peut également être mémorisé avec useMemo ou useCallback pour éviter les rediffusions inutiles lorsqu'il est passé en tant que prop:

const eventListener = useCallback(() => {
  console.log(state);
}, [state]);

Édition précédente de la réponse suggérant d'utiliser l'état mutable applicable à la mise en œuvre initiale de useState hook dans React version 16.7.0-alpha mais n'est pas réalisable en version finale React 16.8 implémentation. useState ne supporte actuellement que l'état immuable.

28
Estus Flask