I'm playing around with React hooks and faced a problem. It shows the wrong state when I'm trying to console log it using button handled by event listener.
CodeSandbox: https://codesandbox.io/s/lrxw1wr97m
- Click on 'Add card' button 2 times
- In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)
- In first card, click on Button2 (handled by event listener) and see in console that there are only 1 card in state (wrong behaviour)
Why does it show the wrong state? In first card, Button2 should display 2 cards in console. Any ideas?
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")
);
I use React 16.7.0-alpha.0 and Chrome 70.0.3538.110
BTW, if I rewrite the CardsProvider using сlass, the problem is gone. CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
This is common problem for functional components that use
useState
hook.handleCardClick
andhandleButtonClick
are defined in the scope ofCardsProvider
functional component. There are new functions each time it runs, they refer tocards
state that was obtained at the moment when they were defined.handleCardClick
handler is registered onCard
mount, it's same function during entire component lifespan and refers to stale state that was fresh at the time whenhandleCardClick
was defined. WhilehandleButtonClick
is registered on eachCard
render, it's new function each time and refers to fresh state.A common approach that addresses this problem is to use
useRef
instead ofuseState
. A ref is a basically recipe that provides a mutable object that can be passed by reference.useRef
is not suitable here because it doesn't re-render a component whileuseState
does.A workaround is to use mutable state that can be passed by reference, similarly to a ref, e.g.:
getCards
is supposed to be be used instead ofcards
to get current state.In this case there's already an object (
cards
array), it can be passed by reference instead of being immutable: