简体   繁体   English

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

[英]Wrong React hooks behaviour with event listener

I'm playing around with React Hooks and am facing a problem.我正在玩React Hooks ,但遇到了问题。 It shows the wrong state when I'm trying to console log it using a button handled by event listener.当我尝试使用事件侦听器处理的按钮来控制台记录它时,它显示错误的 state。

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

  1. Click on 'Add card' button 2 times单击“添加卡”按钮 2 次
  2. In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)在第一张卡片中,单击Button1并在控制台中看到 state 中有 2 张卡片(正确行为)
  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)在第一张卡片中,单击Button2 (由事件侦听器处理)并在控制台中看到 state 中只有一张卡片(错误行为)

Why does it show the wrong state?为什么会显示错误的state?
In first card, Button2 should display 2 cards in the console.在第一张卡片中, Button2应该在控制台中显示2张卡片。 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我正在使用 React 16.7.0-alpha.0 和 Chrome 70.0.3538.110

BTW, if I rewrite the CardsProvider using a сlass, the problem is gone.顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。 CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl CodeSandbox使用class: https://codesandbox.io/s/w2nn3mq9vl

This is a common problem for functional components that use the useState hook.这是使用useState挂钩的功能组件的常见问题。 The same concerns are applicable to any callback functions where useState state is used, eg setTimeout or setInterval timer functions .同样的问题适用于任何使用useState状态的回调函数,例如setTimeoutsetInterval计时器函数

Event handlers are treated differently in CardsProvider and Card components.事件处理程序在CardsProviderCard组件中的处理方式不同。

handleCardClick and handleButtonClick used in the CardsProvider functional component are defined in its scope. CardsProvider功能组件中使用的handleCardClickhandleButtonClick是在其范围内定义的。 There are new functions each time it runs, they refer to cards state that was obtained at the moment when they were defined.每次运行时都会有新的函数,它们指的是在定义它们时获得的cards状态。 Event handlers are re-registered each time the CardsProvider component is rendered.每次呈现CardsProvider组件时都会重新注册事件处理程序。

handleCardClick used in the Card functional component is received as a prop and registered once on component mount with useEffect .Card功能组件中使用的handleCardClick作为道具被接收,并在组件挂载时使用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.它在整个组件生命周期中都是相同的函数,并且指的是在第一次定义handleCardClick函数时新鲜的陈旧状态。 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. handleButtonClick作为 prop 接收并在每次Card渲染时重新注册,每次都是一个新函数,并引用新状态。

Mutable state可变状态

A common approach that addresses this problem is to use useRef instead of useState .解决此问题的常用方法是使用useRef而不是useState A ref is basically a recipe that provides a mutable object that can be passed by reference: ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:

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.在这种情况下,组件应该在状态更新时重新渲染,就像useState所期望的那样,refs 不适用。

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

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

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.这不适用于异步副作用,尤其是async函数。

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 :除非事件侦听器注册在documentwindow或其他超出当前组件范围的事件目标上,否则必须尽可能使用 React 自己的 DOM 事件处理,这消除了对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:在最后一种情况下,可以使用useMemouseCallback对事件监听器进行额外的记忆,以防止在作为道具传递时不必要的重新渲染:

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.此答案的先前版本建议使用可变状态,该状态适用于 React 16.7.0-alpha 版本中的初始useState挂钩实现,但在最终 React 16.8 实现中不适用。 useState currently supports only immutable state.* useState目前仅支持不可变状态。*

A much cleaner way to work around this is to create a hook I call useStateRef解决此问题的一种更简洁的方法是创建一个我称为useStateRef的钩子

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

  const ref = useRef(value);

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

  return [value, setValue, ref];
}

You can now use the ref as a reference to the state value.您现在可以使用ref作为对状态值的引用。

Short answer for me was that useState has a simple solution for this:对我来说简短的回答是 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}));
  }

  //...
}

Short answer for me对我的简短回答

this WILL NOT not trigger re-render ever time myvar changes.不会在 myvar 更改时触发重新渲染。

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

This WILL trigger render -> putting myvar in []这将触发渲染 -> 将myvar放入 []

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

Check the console and you'll get the answer:检查控制台,你会得到答案:

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

Just add props.handleCardClick to the array of dependencies and it will work correctly.只需将props.handleCardClick添加到依赖项数组中,它就会正常工作。

This way your callback will have updated state values always ;)这样,您的回调将始终更新状态值;)

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

    const parentNode = elementRef.current.parentNode

    parentNode.addEventListener('mouseleave', handleAutoClose)

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

}, [handleAutoClose])

To build off of Moses Gitau's great answer , if you are developing in Typescript, to resolve type errors make the hook function generic:为了建立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];
}

Starting from the answer of @Moses Gitau, I'm using a sligthly different one that doesn't give access to a "delayed" version of the value (which is an issue for me) and is a bit more minimalist:从@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;

This is what I'm using这就是我正在使用的

Example of usage:使用示例:

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

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

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

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

Edit: It now gives getValue and not the reference itself.编辑:它现在给出 getValue 而不是引用本身。 I find it better to keep things more encapsulated in that case.我发现在那种情况下最好将事情封装起来。

after changing the following line in the index.js file the button2 works well:更改index.js文件中的以下行后, button2运行良好:

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

you should not use [] as 2nd argument useEffect unless you want it to run once.你不应该使用[]作为第二个参数useEffect除非你希望它运行一次。

more details: https://reactjs.org/docs/hooks-effect.html更多细节: https ://reactjs.org/docs/hooks-effect.html

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

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

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