[英]useState() within useEffect() in a functional component
I tried to change a class based component into a functional component and run into troubles when I tried to read state properties inside a handler function attached with useEffect()
我尝试将基于类的组件更改为功能组件,并在尝试读取附加有
useEffect()
的处理函数中的状态属性时遇到麻烦
Inside the class based component I'll attach the handler method inside componentDidMount
and this handler has access to the state. 该类基于组件里面,我会附上里面的处理方法
componentDidMount
这个处理程序可以访问的状态。
The rendered output does show the correct values in both types of components! 渲染的输出在两种类型的组件中均显示正确的值!
But when I need the current value of the state property inside my handler to calculate the new state values I run into troubles reading the state properties inside functional components. 但是,当我需要处理程序内部的state属性的当前值来计算新的状态值时,我会遇到读取功能组件内部的state属性的麻烦。
I created following example to demonstrate the problem: https://codesandbox.io/s/peaceful-wozniak-l6w2c (open console and scroll) 我创建了以下示例来演示该问题: https : //codesandbox.io/s/peaceful-wozniak-l6w2c (打开控制台并滚动)
If you click on the components both work fine: 如果您单击两个组件都可以正常工作:
counter
counter
的当前值 counter
counter
If you scroll only the class based component works: 如果滚动仅基于类的组件有效:
position
position
的当前值 position
position
But the console output from the functional components only returns me the initial state property value. 但是功能组件的控制台输出仅向我返回初始状态属性值。
class based component 基于类的组件
import React, { Component } from "react";
export default class CCTest extends Component {
constructor(props) {
super(props);
this.state = { position: 0, counter: 0 };
this.handleScroll = this.handleScroll.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleScroll(e) {
console.log(
"class.handleScroll()",
this.state.position
);
this.setState({ position: document.body.getBoundingClientRect().top });
}
handleClick() {
console.log("[class.handleClick]", this.state.counter);
this.setState(prevState => ({ counter: prevState.counter + 1 }));
}
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
render() {
return (
<div
style={{
backgroundColor: "orange",
padding: "20px",
cursor: "pointer"
}}
onClick={this.handleClick}
>
<strong>class</strong>
<p>
position: {this.state.position}, counter: {this.state.counter}
</p>
</div>
);
}
}
functional component 功能组件
import React from "react";
export default prop => {
const [position, setPosition] = React.useState(0);
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
// eslint-disable-next-line
}, []);
const handleScroll = () => {
console.log(
"function.handleScroll()",
position
);
setPosition(document.body.getBoundingClientRect().top);
};
const handleClick = () => {
console.log("[function.handleClick]", counter);
setCounter(counter + 1);
};
return (
<div
style={{ backgroundColor: "green", padding: "20px", cursor: "pointer" }}
onClick={handleClick}
>
<strong>function</strong>
<p>
position: {position}, counter: {counter}
</p>
</div>
);
};
Every little hint helps <3 每个小提示都会帮助<3
Every time your component renders, a new handleScroll function is created, which has access through the component closure to the current state. 每次渲染组件时,都会创建一个新的handleScroll函数,该函数可以通过组件闭包访问当前状态。 But this closure doesn't get updated on every render;
但是这种关闭不会在每个渲染器上都得到更新。 instead, a new handleScroll function is created, which sees the new values.
而是创建一个新的handleScroll函数,该函数可以查看新值。
The problem is that when you do: 问题是您这样做时:
window.addEventListener("scroll", handleScroll);
You're binding the current version of the function to the event, and it will always read the values at that time, instead of new ones. 您将函数的当前版本绑定到该事件,并且该事件将始终在该时间读取值,而不是新值。 Normally, useEffect would be run again and a new instance of the function would be used, but you're preventing it with the empty array as the second parameter.
通常,useEffect将再次运行,并且将使用该函数的新实例,但是您要使用空数组作为第二个参数来阻止它。
There is a linter warning just for these cases, but you have disabled it: 仅在这些情况下会有棉绒警告,但您已将其禁用:
// eslint-disable-next-line
If you remove this empty array, you can remove the linter disabling rule, and it will work properly. 如果删除此空数组,则可以删除禁用linter规则,该规则将正常运行。
Other options 其他选择
When having the function change every time is not an option (for example, when using non-react libraries, or for performance reasons), there are other alternatives: 如果无法每次都更改功能(例如,使用非反应库或出于性能原因),则还有其他选择:
You can use a ref instead of state. 您可以使用ref代替state。 Refs get mutated instead of a new one being created on every render.
引用会发生突变,而不是在每个渲染上都创建一个新引用。 This allows previous versions of the function to read the current value, and modify it, even if you don't update the function.
即使您不更新功能,这也允许该功能的先前版本读取当前值并进行修改。
You can use useReducer . 您可以使用useReducer 。 The dispatch function never changes, so if your useEffect depends on it, it won't be re-run.
调度功能永远不会改变,因此,如果您的useEffect依赖它,它将不会重新运行。 The reducer can access both the previous value and the action, so it can calc the new state even if it depends on the previous one.
Reducer可以访问先前的值和操作,因此即使依赖于先前的值,它也可以计算新状态。
Thanks to the help of @Anxo I ended up ditching useState
and utilize useRecuder
instead. 多亏了@Anxo的帮助,我最终放弃了
useState
并useRecuder
。
This is my functional component for the specific example in the initial question: 这是第一个问题中特定示例的功能组件 :
import React from 'react';
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_POSITION':
return {
...state,
position: document.body.getBoundingClientRect().top
};
case 'INCREMENT_COUNTER':
return {
...state,
counter: state.counter + 1
};
default:
throw new Error('chalupa batman');
}
};
const initialState = {
position: 0,
counter: 0
};
const FCTest = props => {
const [state, dispatch] = React.useReducer(reducer, initialState);
React.useEffect(() => {
window.addEventListener('scroll', () => {
dispatch({ type: 'UPDATE_POSITION' });
});
return () => {
window.removeEventListener(
'scroll',
dispatch({ type: 'UPDATE_POSITION' })
);
};
// eslint-disable-next-line
}, []);
return (
<div
style={{ backgroundColor: 'green', padding: '20px', cursor: 'pointer' }}
onClick={() => dispatch({ type: 'INCREMENT_COUNTER' })}
>
<strong>function</strong>
<p>
position: {state.position}, counter: {state.counter}
</p>
</div>
);
};
export default FCTest;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.