Wrong React hooks behaviour with event listener

2019-02-18 00:59发布

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

  1. Click on 'Add card' button 2 times
  2. In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)
  3. 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

1条回答
女痞
2楼-- · 2019-02-18 01:47

This is common problem for functional components that use useState hook.

handleCardClick and handleButtonClick are defined in the scope of CardsProvider functional component. There are new functions each time it runs, they refer to cards state that was obtained at the moment when they were defined.

handleCardClick handler is registered on Card mount, it's same function during entire component lifespan and refers to stale state that was fresh at the time when handleCardClick was defined. While handleButtonClick is registered on each Card render, it's 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 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 while useState does.

A workaround is to use mutable state that can be passed by reference, similarly to a ref, e.g.:

  const [cardsState, setCardsState] = useState({ state: [] })

  const getCards = () => cardsState.state
  const setCards = v => {
    cardsState.state = v;
    setCardsState(cardsState)
  }

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

  const handleCardClick = id => {
    console.log(getCards())
  }

getCards is supposed to be be used instead of cards to get current state.

In this case there's already an object (cards array), it can be passed by reference instead of being immutable:

  const [cards, setCards] = useState([])

  const addCard = () => {
    const id = cards.length
    cards.push({ id: id, json: { } })
    setCards(cards)
  }
查看更多
登录 后发表回答