错误的React与事件侦听器挂钩行为

Mar*_*ano 20 javascript reactjs react-hooks

我正在玩React钩子并面临一个问题.当我尝试使用事件监听器处理的按钮来控制日志时,它显示错误的状态.

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

  1. 点击"添加卡片"按钮2次
  2. 在第一张卡中,单击Button1并在控制台中看到有2张卡处于状态(正确行为)
  3. 在第一张卡片中,单击Button2(由事件监听器处理)并在控制台中看到状态中只有1张卡片(行为错误)

为什么它显示错误的状态?在第一张卡中,Button2应在控制台中显示2张卡.有任何想法吗?

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")
);
Run Code Online (Sandbox Code Playgroud)

我使用React 16.7.0-alpha.0和Chrome 70.0.3538.110

顺便说一句,如果我使用сlass重写CardsProvider,问题就消失了.CodeSandbox使用类:https://codesandbox.io/s/w2nn3mq9vl

Est*_*ask 35

这是使用useState钩子的功能组件的常见问题.

useStatesetTimeout定义在setInterval功能组件的范围内.每次运行时都有新功能,它们指的CardsProvider是在定义它们时获得的状态.

Cardhandler在handleCardClickmount 上注册,它在整个组件生命周期内具有相同的功能,并且指的是在handleButtonClick定义时新鲜的陈旧状态.虽然CardsProvider在每个cards渲染上注册,但它每次都是新功能,并指新鲜状态.

解决此问题的常用方法是使用CardsProvider而不是handleCardClick.ref是一个基本配方,它提供了一个可以通过引用传递的可变对象.Card这里不适合,因为它不会重新渲染组件useEffect.

解决方法是使用可以通过引用传递的可变状态,类似于ref,例如:

const ref = useRef(0);

function eventListener() {
  ref.current++;
}
Run Code Online (Sandbox Code Playgroud)

handleCardClick应该被用来代替handleButtonClick获得当前状态.

在这种情况下,已经存在一个对象(Card数组),它可以通过引用传递而不是不可变的:

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

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

function eventListener() {
  ref.current++;
  forceUpdate();
}
Run Code Online (Sandbox Code Playgroud)

  • React 文档中也解释了这个问题:https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function (4认同)
  • “手动事件侦听器重新注册” - 需要在每次重新注册之前取消注册,以消除重复调用。 (2认同)

小智 26

解决这个问题的更简洁的方法是创建一个我调用的钩子 useStateRef

function useStateRef(initialValue) {
  const [value, setValue] = useState(initialValue);

  const ref = useRef(value);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return [value, setValue, ref];
}
Run Code Online (Sandbox Code Playgroud)

您现在可以将ref用作状态值的引用。


Che*_*les 7

对我来说简短的回答是 useState 有一个简单的解决方案:

function Example() {
  const [state, setState] = useState(initialState);

  function update(updates) {
    // this might be stale
    setState({...state, ...updates});
    // but you can pass setState a function instead
    setState(currentState => ({...currentState, ...updates}));
  }

  //...
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为您错过了更大的图景,如果您不想 setState 您只想使用状态来返回特定值或想要映射状态的特定属性,该怎么办?您的答案只是更大答案的一部分。 (2认同)

Nel*_*les 6

对我的简短回答

不会在myvar 更改时触发重新渲染。

const [myvar, setMyvar] = useState('')
  useEffect(() => {    
    setMyvar('foo')
  }, []);
Run Code Online (Sandbox Code Playgroud)

触发渲染 -> 将myvar 放入 []

const [myvar, setMyvar] = useState('')
  useEffect(() => {    
    setMyvar('foo')
  }, [myvar]);
Run Code Online (Sandbox Code Playgroud)

  • 不,两者 **都会** 触发重新渲染,只是第一个 `useEffect` 将在组件安装时触发一次,而第二个 `useEffect` 将在每次 `myvar` 更新时触发。 (2认同)