错误的React将事件监听器的行为钩住 [英] Wrong React hooks behaviour with event listener

查看:35
本文介绍了错误的React将事件监听器的行为钩住的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在玩 React hooks 并遇到问题. 当我尝试使用事件监听器处理的按钮来控制台记录日志时,它显示错误的状态.

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.

代码沙箱: https://codesandbox.io/s/lrxw1wr97m

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

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

Why does it show the wrong state?
In first card, Button2 should display 2 cards in 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>

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

I use React 16.7.0-alpha.0 and Chrome 70.0.3538.110

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

BTW, if I rewrite the CardsProvider using сlass, the problem is gone. CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl

推荐答案

对于使用useState钩子的功能组件,这是常见问题.同样的问题也适用于使用useState状态的任何回调函数,例如 setTimeoutsetInterval计时器函数.

This is common problem for functional components that use useState hook. The same concerns are applicable to any callback functions where useState state is used, e.g. setTimeout or setInterval timer functions.

事件处理程序在CardsProviderCard组件中的处理方式有所不同.

Event handlers are treated differently in CardsProvider and Card components.

handleCardClickhandleButtonClick在其范围内定义.每次运行时都有新功能,它们引用在定义它们时获得的cards状态.每次渲染CardsProvider组件时,事件处理程序都会重新注册.

handleCardClick and handleButtonClick used in 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 CardsProvider component is rendered.

handleCardClick作为道具被接收,并在useEffect的组件安装中注册一次.它在整个组件寿命期间都具有相同的功能,并且是指在第一次定义handleCardClick函数时新鲜的陈旧状态. handleButtonClick作为道具被接收并在每个Card渲染器上重新注册,每次都是新功能,并引用新鲜状态.

handleCardClick used in Card functional component is received as a prop and registered once on component mount with useEffect. It's the same function during entire component lifespan and refers to stale state that was fresh at the time when 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.

解决此问题的常用方法是使用useRef而不是useState.引用基本上是一种配方,提供了可以通过引用传递的可变对象:

A common approach that addresses this problem is to use useRef instead of useState. A ref is a basically a recipe that provides a mutable object that can be passed by reference:

const ref = useRef(0);

function eventListener() {
  ref.current++;
}

如果组件应该在状态更新时如useState中所预期的那样重新呈现,则引用不适用.

In case a component should be re-rendered on state update like it's expected from useState, refs aren't applicable.

可以分别保持状态更新和可变状态,但是forceUpdate在类和函数组件中都被视为反模式(列出仅供参考):

It's possible to keep state updates and mutable state separately but forceUpdate is considered an antipattern 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();
}

状态更新器功能

一种解决方案是使用状态更新程序功能,该功能从封闭的范围接收新鲜状态而不是陈旧状态:

State updater function

One solution is to use state updater function that receives fresh state instead of stale state from enclosing scope:

function eventListener() {
  // doesn't matter how often the listener is registered
  setState(freshState => freshState + 1);
}

如果需要一个状态来实现同步副作用,例如console.log,一种解决方法是返回相同的状态以防止更新.

In case a state is needed for synchronous side effect 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
  };
}, []);

这不适用于异步副作用,尤其是async函数.

This doesn't work well with asynchronous side effects, notably async functions.

另一种解决方案是每次都重新注册事件侦听器,因此回调总是从封闭范围获得最新状态:

Another solution is to re-register event listener every time, so a callback always gets fresh state from enclosing scope:

function eventListener() {
  console.log(state);
}

useEffect(() => {
  // register eventListener on each state update

  return () => {
    // unregister eventListener
  };
}, [state]);

内置事件处理

除非在document上注册了事件侦听器,或者window或其他事件目标不在当前组件的范围内,否则必须尽可能使用React自己的DOM事件处理,这消除了对useEffect的需要:

Built-in event handling

Unless event listener is registered on document, window or other event targets are outside of the scope of current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect:

<button onClick={eventListener} />

在最后一种情况下,事件监听器还可以通过useMemouseCallback进行记忆,以防止在作为道具传递时不必要的重新渲染:

In the last case 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]);

答案的先前版本建议使用可变状态,该可变状态适用于React 16.7.0-alpha版本中的初始useState钩子实现,但不适用于最终的React 16.8实现. useState当前仅支持不可变状态.

Previous edition of the answer suggested to use mutable state that is applicable to initial 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.

这篇关于错误的React将事件监听器的行为钩住的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆