I'm playing around with React Hooks and am facing a problem. It shows the wrong state when I'm trying to console log it using a button handled by event listener.
CodeSandbox: https://codesandbox.io/s/lrxw1wr97m
Why does it show the wrong state?
In first card, Button2 should display 2 cards in the console. Any ideas?
const { useState, useContext, useRef, useEffect } = React; 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") ); <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <div id='root'></div> I am using React 16.7.0-alpha.0 and Chrome 70.0.3538.110
BTW, if I rewrite the CardsProvider using a сlass, the problem is gone. CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
There are three common reasons you might be seeing it: You might have mismatching versions of React and React DOM. You might be breaking the Rules of Hooks. You might have more than one copy of React in the same app.
1. Changing the Hooks Invocation Order. Hooks should not be called within loops, conditions, or nested functions since conditionally executed Hooks can cause unexpected bugs. Avoiding such situations ensures that Hooks are called in the correct order each time the component renders.
Common side effects include setting the page title imperatively, working with timers like setInterval or setTimeout , measuring the width or height or position of elements in the DOM, logging messages to the console, setting or getting values in local storage, and fetching data or subscribing and unsubscribing to ...
This is a common problem for functional components that use the useState hook. The same concerns are applicable to any callback functions where useState state is used, e.g. setTimeout or setInterval timer functions.
Event handlers are treated differently in CardsProvider and Card components.
handleCardClick and handleButtonClick used in the CardsProvider functional component are defined in its scope. There are new functions each time it runs, they refer to cards state that was obtained at the moment when they were defined. Event handlers are re-registered each time the CardsProvider component is rendered.
handleCardClick used in the Card functional component is received as a prop and registered once on component mount with useEffect. It's the same function during the entire component lifespan and refers to stale state that was fresh at the time when the handleCardClick function was defined the first time. handleButtonClick is received as a prop and re-registered on each Card render, it's a new function each time and refers to fresh state.
A common approach that addresses this problem is to use useRef instead of useState. A ref is basically a recipe that provides a mutable object that can be passed by reference:
const ref = useRef(0); function eventListener() { ref.current++; } In this case a component should be re-rendered on a state update like it's expected from useState, refs aren't applicable.
It's possible to keep state updates and mutable state separately but forceUpdate is considered an anti-pattern in both class and function components (listed for reference only):
const useForceUpdate = () => { const [, setState] = useState(); return () => setState({}); } const ref = useRef(0); const forceUpdate = useForceUpdate(); function eventListener() { ref.current++; forceUpdate(); } One solution is to use a state updater function that receives fresh state instead of stale state from the enclosing scope:
function eventListener() { // doesn't matter how often the listener is registered setState(freshState => freshState + 1); } In this case a state is needed for synchronous side effects like console.log, a workaround is to return the same state to prevent an update.
function eventListener() { setState(freshState => { console.log(freshState); return freshState; }); } useEffect(() => { // register eventListener once return () => { // unregister eventListener once }; }, []); This doesn't work well with asynchronous side effects, notably async functions.
Another solution is to re-register the event listener every time, so a callback always gets fresh state from the enclosing scope:
function eventListener() { console.log(state); } useEffect(() => { // register eventListener on each state update return () => { // unregister eventListener }; }, [state]); Unless the event listener is registered on document, window or other event targets that are outside of the scope of the current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect:
<button onClick={eventListener} /> In the last case the event listener can be additionally memoized with useMemo or useCallback to prevent unnecessary re-renders when it's passed as a prop:
const eventListener = useCallback(() => { console.log(state); }, [state]); useState hook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation. useState currently supports only immutable state.*If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With