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
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
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.
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();
}
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
.
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]);
À 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.