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

时间:2018-12-19 06:21:37

标签: javascript reactjs react-hooks

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

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

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

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

import React, { useState, useContext, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

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

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

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

7 个答案:

答案 0 :(得分:11)

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

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

handleCardClick功能组件中使用的

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

handleCardClick功能组件中使用的

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

可变状态

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

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
}, []);

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

手动事件侦听器重新注册

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

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

useEffect(() => {
  // register eventListener on each state update
}, [state]);

内置事件处理

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

<button onClick={eventListener} />

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

const eventListener = useCallback(() => {
  console.log(state);
}, [state]);

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

答案 1 :(得分:3)

一种更清洁的解决方法是创建一个我称为useStateRef

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

  const ref = useRef(value);

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

  return [value, setValue, ref];
}

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

答案 2 :(得分:2)

对我来说简短的回答

将不会 不会触发重新提交

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

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

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

答案 3 :(得分:0)

IMO,第一个答案应该是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}));
  }

  //...
}

在先前的回答中,我被tl; dr烧死了。

答案 4 :(得分:0)

检查控制台,您将得到答案:

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

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

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

    const parentNode = elementRef.current.parentNode

    parentNode.addEventListener('mouseleave', handleAutoClose)

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

}, [handleAutoClose])