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



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

代码沙盒: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 = 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

顺便说一句,如果我使用 сlass 重写卡提供商,问题就消失了。 使用类的代码沙盒:https://codesandbox.io/s/w2nn3mq9vl

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

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

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

Card功能组件中使用的handleCardClick作为道具接收,并在组件安装useEffect上注册一次。它在整个组件生命周期中是相同的函数,指的是首次定义handleCardClick函数时新鲜的过时状态。handleButtonClick作为道具接收并在每次渲染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 事件处理,这消除了对useEffect的需求:

<button onClick={eventListener} />

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

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

相关内容

  • 没有找到相关文章

最新更新