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

Posted

技术标签:

【中文标题】错误的 React 将行为与事件侦听器挂钩【英文标题】:Wrong React hooks behaviour with event listener 【发布时间】:2019-05-19 14:42:22 【问题描述】:

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

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

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

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

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

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

【问题讨论】:

【参考方案1】:

这是使用useState 挂钩的功能组件的常见问题。同样的问题适用于使用useState 状态的任何回调函数,例如setTimeout or setInterval timer functions.

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

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

Card 功能组件中使用的handleCardClick 作为prop 接收并在组件安装时注册一次useEffect。它在整个组件生命周期内都是同一个函数,指的是在第一次定义 handleCardClick 函数时新鲜的陈旧状态。 handleButtonClick 作为 props 接收并在每个 Card 渲染上重新注册,它每次都是一个新函数,并且指的是新状态。

可变状态

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

const ref = useRef(0);

function eventListener() 
  ref.current++;

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

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

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


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

function eventListener() 
  ref.current++;
  forceUpdate();

状态更新函数

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

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

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

function eventListener() 
  setState(freshState => 
    console.log(freshState);
    return freshState;
  );


useEffect(() => 
  // register eventListener once

  return () => 
    // unregister eventListener once
  ;
, []);

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

手动事件监听器重新注册

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

function eventListener() 
  console.log(state);


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

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

内置事件处理

除非事件监听器注册在documentwindow或其他当前组件范围之外的事件目标上,否则必须尽可能使用React自己的DOM事件处理,这样就不需要@ 987654353@:

<button onClick=eventListener />

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

const eventListener = useCallback(() => 
  console.log(state);
, [state]);
此答案的先前版本建议使用可变状态,该状态适用于 React 16.7.0-alpha 版本中的初始 useState 挂钩实现,但在最终 React 16.8 实现中不可用。 useState 目前仅支持不可变状态。*

【讨论】:

React 文档中也解释了这个问题:reactjs.org/docs/… Manual event listener re-registration - 每次重新注册前都需要注销,以消除重复调用。 @vsync 假定侦听器在某些时候未注册,因为永远不需要不进行清理的组件。卸载时应该已经有取消注册代码,是的,在这种情况下应该多次执行。为清晰起见进行了更新。【参考方案2】:

给我一​​个简短的答案

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

const [myvar, setMyvar] = useState('')
  useEffect(() =>     
    setMyvar('foo')
  , []);

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

const [myvar, setMyvar] = useState('')
  useEffect(() =>     
    setMyvar('foo')
  , [myvar]);

【讨论】:

不,两个 WILL 都会触发重新渲染,只是第一个 useEffect 会在组件挂载时触发一次,而第二个会在每次 myvar 更新时触发.【参考方案3】:

这样,您的回调将始终更新状态值;)

 // registers an event listener to component parent
 React.useEffect(() => 

    const parentNode = elementRef.current.parentNode

    parentNode.addEventListener('mouseleave', handleAutoClose)

    return () => 
        parentNode.removeEventListener('mouseleave', handleAutoClose)
    

, [handleAutoClose])

【讨论】:

【参考方案4】:

index.js 文件中更改以下行后,button2 运行良好:

useEffect(() => 
    ref.current.addEventListener("click", props.handleCardClick);
    return () => 
        ref.current.removeEventListener("click", props.handleCardClick);
    ;
- , []);
+ );

你不应该使用[] 作为第二个参数useEffect,除非你希望它运行一次。

更多详情:https://reactjs.org/docs/hooks-effect.html

【讨论】:

这种行为等同于 componentDidUpdate 并且在每次重新渲染时 eventListener 将被删除然后重新创建,这是不必要的并且会影响性​​能。可以向卡片添加依赖项,以便添加和删除事件侦听器不会太多,或者使用 useRef 来获取卡片的可变值。【参考方案5】:

对我来说,简短的回答是 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));
  

  //...

【讨论】:

我认为你错过了一个更大的图景,如果你不想 setState 你只想消费状态以返回一个特定的值或想要映射到状态的特定属性。你的答案只是一个更大的答案的一部分。 @ncubica,绝对是,但其他答案都强调参考和效果,我想强调你可能不需要它们。我第一次错过了这个。【参考方案6】:

检查控制台,你会得到答案:

React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)

只需将props.handleCardClick 添加到依赖项数组中即可正常工作。

【讨论】:

【参考方案7】:

解决这个问题的一种更简洁的方法是创建一个我称之为useStateRef的钩子

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

  const ref = useRef(value);

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

  return [value, setValue, ref];

您现在可以使用ref 作为对状态值的引用。

【讨论】:

优秀的解决方案!谢谢先生,您的解决方案完美解决了我的问题。 这完全解决了问题,也超级干净。只需使用事件处理程序中的 refs 和 jsx 中的状态。喜欢它。 这个参考值是更新后的值吗?还是之前的值? @KamalHossain 此方法返回当前状态值的引用。您应该将您的状态重构为 [state, setState, stateRef] 并使用此方法而不是 useState(initialValue) 为什么会有useEffect? ref.current 将比值更改的渲染上的当前值低 1 个值。我认为简单地说ref.current = value 更正确-这没有考虑到OP的问题-只是这个钩子是如何制定的

以上是关于错误的 React 将行为与事件侦听器挂钩的主要内容,如果未能解决你的问题,请参考以下文章

将事件侦听器添加到我的页面会导致问题

react-datepicker“无法阻止被动事件侦听器调用中的默认值”

调度自定义事件并测试它是不是被正确触发(React TypeScript,Jest)

将 Numpad 与修改键一起使用会表现出奇怪的行为

Java——监听器

JavaWeb学习笔记八 监听器