错误的 React 使用事件侦听器挂钩行为 [英] Wrong React hooks behaviour with event listener

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

问题描述

我正在使用 React Hooks 并且遇到了一个问题.当我尝试使用由事件侦听器处理的按钮进行控制台记录时,它显示了错误的状态.

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

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

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

const { useState, useContext, useRef, useEffect } = React;const CardsContext = React.createContext();const CardsProvider = props =>{const [cards, setCards] = useState([]);const addCard = () =>{const id = 卡片长度;setCards([...cards, { id: id, json: {} }]);};const handleCardClick = id =>控制台日志(卡片);const handleButtonClick = id =>控制台日志(卡片);返回 (<CardsContext.Providervalue={{ 卡片,addCard,handleCardClick,handleButtonClick }}>{props.children}</CardsContext.Provider>);};功能应用(){const { 卡片,addCard,handleCardClick,handleButtonClick } = useContext(卡片上下文);返回 (<div className="应用程序"><button onClick={addCard}>添加卡片</button>{cards.map((card, index) => (<卡片密钥={card.id}id={card.id}handleCardClick={() =>handleCardClick(card.id)}handleButtonClick={() =>handleButtonClick(card.id)}/>))}

);}功能卡片(道具){const ref = useRef();useEffect(() => {ref.current.addEventListener("click", props.handleCardClick);返回 () =>{ref.current.removeEventListener("click", props.handleCardClick);};}, []);返回 (<div className="card">卡片 {props.id}<div><button onClick={props.handleButtonClick}>Button1</button><按钮引用={节点=>(ref.current = node)}>Button2</button>

);}ReactDOM.render(<卡片供应商><应用程序/></CardsProvider>,document.getElementById("root"));

<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

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

解决方案

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

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

CardsProvider 功能组件中使用的

handleCardClickhandleButtonClick 定义在其作用域内.每次运行都会有新的函数,它们指的是在定义它们时获得的 cards 状态.每次呈现 CardsProvider 组件时都会重新注册事件处理程序.

Card 功能组件中使用的

handleCardClick 作为 prop 接收并在组件挂载时使用 useEffect 注册一次.它在整个组件生命周期中都是相同的函数,并且指的是第一次定义 handleCardClick 函数时新鲜的陈旧状态.handleButtonClick 作为 prop 接收并在每个 Card 渲染时重新注册,每次都是一个新函数,并引用新状态.

可变状态

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

const ref = useRef(0);函数事件监听器(){ref.current++;}

在这种情况下,组件应该在状态更新时重新渲染,就像 useState 预期的那样,引用不适用.

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

const useForceUpdate = () =>{const [, setState] = useState();返回 () =>设置状态({});}const ref = useRef(0);const forceUpdate = useForceUpdate();函数事件监听器(){ref.current++;强制性升级();}

状态更新函数

一种解决方案是使用状态更新器函数,该函数从封闭范围接收新鲜状态而不是陈旧状态:

function eventListener() {//监听器注册的频率无关紧要setState(freshState => freshState + 1);}

在这种情况下,像 console.log 这样的同步副作用需要一个状态,解决方法是返回相同的状态以防止更新.

function eventListener() {设置状态(新鲜状态 => {控制台日志(新鲜状态);返回新鲜状态;});}useEffect(() => {//注册一次事件监听器返回 () =>{//取消注册 eventListener 一次};}, []);

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

手动事件监听器重新注册

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

function eventListener() {控制台日志(状态);}useEffect(() => {//在每次状态更新时注册 eventListener返回 () =>{//注销事件监听器};}, [状态]);

内置事件处理

除非在 documentwindow 或当前组件范围之外的其他事件目标上注册事件监听器,否则 React 自己的 DOM 事件处理必须在可能的情况下使用,这消除了对 useEffect 的需要:

在最后一种情况下,事件侦听器可以使用 useMemouseCallback 进行额外记忆,以防止在作为 prop 传递时进行不必要的重新渲染:

const eventListener = useCallback(() => {控制台日志(状态);}, [状态]);

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

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

  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 is only 1 card in state (wrong behaviour)

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

解决方案

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.

Mutable 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();
}

State updater function

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.

Manual event listener re-registration

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]);

Built-in event handling

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]);

  • Previous edition of this answer suggested to use mutable state that was 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天全站免登陆