繁体   English   中英

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

[英]Wrong React hooks behaviour with event listener

我正在玩React Hooks ,但遇到了问题。 当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示错误的 state。

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

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

为什么会显示错误的state?
在第一张卡片中, 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使用class: https://codesandbox.io/s/w2nn3mq9vl

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

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

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

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

可变状态

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

const ref = useRef(0);

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

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

可以分别保持状态更新和可变状态,但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 事件处理,这消除了对useEffect的需要:

<button onClick={eventListener} />

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

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

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

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

  const ref = useRef(value);

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

  return [value, setValue, ref];
}

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

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

  //...
}

对我的简短回答

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

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

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

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

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

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

只需将props.handleCardClick添加到依赖项数组中,它就会正常工作。

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

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

    const parentNode = elementRef.current.parentNode

    parentNode.addEventListener('mouseleave', handleAutoClose)

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

}, [handleAutoClose])

为了建立Moses Gitau 的出色答案,如果您正在使用 Typescript 进行开发,为了解决类型错误,请使用通用钩子函数:

function useStateRef<T>(initialValue: T | (() => T)): 
   [T, React.Dispatch<React.SetStateAction<T>>, React.MutableRefObject<T>] {
  const [value, setValue] = React.useState(initialValue);

  const ref = React.useRef(value);

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

  return [value, setValue, ref];
}

从@Moses Gitau 的回答开始,我使用的是一个略有不同的版本,它不能访问值的“延迟”版本(这对我来说是个问题)并且更加简约:

import { useState, useRef } from 'react';

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

    const ref = useRef(initialValue);

    const setValue = (val) => {
        ref.current = val;
        setValueState(val); // to trigger the refresh
    };

    const getValue = (val) => {
        return ref.current;
    };

    return [getValue , setValue];
}
export default useStateRef;

这就是我正在使用的

使用示例:

const [getValue , setValue] = useStateRef(0);

const listener = (event) => {
    setValue(getValue() + 1);
};

useEffect(() => {
    window.addEventListener('keyup', listener);

    return () => {
        window.removeEventListener('keyup', listener);
    };
}, []);

编辑:它现在给出 getValue 而不是引用本身。 我发现在那种情况下最好将事情封装起来。

更改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

我遇到了类似的问题,我的事件函数从上下文中获取了过时的值,这篇文章帮助了我。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM