我有一个在滚动事件上侦听的函数handleScroll
。Thsi 函数必须更新isFetching
(从 false 开始,必须更改布尔值)。
handleScroll
函数被正确侦听,如console.log
所示。但是,isFetching
总是错误的。 似乎setIsFetching
从未被阅读过。我认为,另一种选择就像 eventListener 冻结了 handleScroll 函数的第一个版本。
我该怎么做才能更新该函数中的钩子?这是代码和代码沙箱的简化版本:
/* <div id='root'></div> */
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const debounce = (func, wait, immediate) => {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
};
const App = () => {
const [isFetching, setIsFetching] = useState(false);
const handleScroll = debounce(() => {
setIsFetching(!isFetching);
console.log({ isFetching });
}, 300);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return <div style={{ height: "1280px" }}>Hello world</div>;
};
const root = document.getElementById("root");
if (root) ReactDOM.render(<App />, root);
更新
我在useEffect
中放置了一个空数组作为第二个参数,因为我希望第一个参数函数只在组件DidMount()上触发一次
将isFetching
作为依赖项添加到useEffect
虽然我不能提供深入的解释,但我可以说你基本上在useEffect
中对 React 撒谎,当你说效果不依赖于任何东西时,通过提供一个空的依赖项数组,传递所有变量总是好的。
此外,每次组件re-render
时,您都会创建一个新函数,以避免这种情况,将函数移动到useEffect
内部或将其包装在useCallback
这不会创建重新创建函数,除非依赖项数组中的某些内容发生变化
useEffect(
() => {
const handleScroll = debounce(() => {
setIsFetching(prevState => !prevState);
console.log({ isFetching });
}, 300);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
},
[isFetching]
);
或与useCallback
const handleScroll = useCallback(
debounce(() => {
setIsFetching(prevState => !prevState);
console.log({ isFetching });
}, 300),
[isFetching]
);
useEffect(
() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
},
[isFetching]
);
使用效果完整指南
为了从useEffect
回调内部监听状态的变化(当你不关注任何 props 更新时),你可以将你的状态保存在组件范围之外的变量中,并直接使用它代替状态。
这里有代码:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const debounce = (func, wait, immediate) => {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
};
let isFetchingState;
const App = () => {
const [isFetching, setIsFetching] = useState(false);
isFetchingState = isFetching;
const handleScroll = debounce(() => {
setIsFetching(!isFetchingState);
console.log({ isFetchingState });
}, 300);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return <div style={{ height: "1280px" }}>Hello world</div>;
};
const root = document.getElementById("root");
if (root) ReactDOM.render(<App />, root);
你已经传递了一个空数组作为第二个参数来使用 Effect。这就是您在运行后不再调用useEffect的关键问题。
要解决这个问题,只是不要传递第二个参数。
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
或者,每当更改属性时调用 useEffect:
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [isFetching]); // re-run useEffect on isFetching changed
这类似于我们在 componentDidUpdate 中所做的:
if (prevState.count !== this.state.count) {
// do the stuff
有关更多详细信息,请参阅文档本身。
文档中的注释:
如果要运行效果并仅清理一次(在装载和卸载时),则可以传递空数组 ([]) 作为第二个参数。这告诉 React 你的效果不依赖于来自 props 或状态的任何值,所以它永远不需要重新运行。这不是作为特殊情况处理的 - 它直接遵循依赖关系数组始终的工作方式。
如果传递一个空数组 ([]),效果中的 props 和状态将始终具有其初始值。虽然传递 [] 作为第二个参数更接近熟悉的组件 DidMount 和组件 WillUnmount 心智模型,但通常有更好的解决方案来避免过于频繁地重新运行效果。另外,不要忘记 React 将运行 useEffect 推迟到浏览器绘制之后,因此做额外的工作不是一个问题。