从滚动事件侦听器中反应 setState 挂钩
React setState hook from scroll event listener
首先,对于 class 组件,这工作正常并且不会导致任何问题。
但是,在带有钩子的功能组件中,每当我尝试从我的滚动事件侦听器的函数 handleScroll
设置状态时,即使我使用了去抖动,我的状态也无法更新或应用程序的性能受到严重影响。
import React, { useState, useEffect } from "react";
import debounce from "debounce";
let prevScrollY = 0;
const App = () => {
const [goingUp, setGoingUp] = useState(false);
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY < currentScrollY && goingUp) {
debounce(() => {
setGoingUp(false);
}, 1000);
}
if (prevScrollY > currentScrollY && !goingUp) {
debounce(() => {
setGoingUp(true);
}, 1000);
}
prevScrollY = currentScrollY;
console.log(goingUp, currentScrollY);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div>
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
</div>
);
};
export default App;
尝试在 handleScroll
函数中使用 useCallback
钩子,但没有太大帮助。
我做错了什么?如何在不对性能产生巨大影响的情况下从 handleScroll
设置状态?
我已经针对这个问题创建了一个沙箱。
你在每个渲染器上都是 re-creating handleScroll
函数,所以它指的是实际数据 (goingup
) 所以你在这里不需要 useCallback
。
但是除了你重新创建 handleScroll
你被设置为事件侦听器只是它的第一个实例 - 因为你给 useEffect(..., [])
它 运行 只在挂载时一次。
一种解决方案是 re-subscribe 到 scroll
和 up-to-date handleScroll
。为此,您必须在 useEffect
中 return 清理函数(现在它 return 又订阅了一个),并在每次渲染时将 []
删除到 运行 :
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
PS 最好使用 passive event listener 滚动。
在你的代码中我发现了几个问题:
1) useEffect 中的 []
意味着它不会看到任何状态变化,例如 goingUp
的变化。它将始终看到 goingUp
的初始值
2) debounce
这样不行。它 returns 一个新的去抖功能。
3) 通常全局变量是一个 anti-pattern,认为它只适用于您的情况。
4) 正如@skyboyer 所提到的,您的滚动侦听器不是被动的。
import React, { useState, useEffect, useRef } from "react";
const App = () => {
const prevScrollY = useRef(0);
const [goingUp, setGoingUp] = useState(false);
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY.current < currentScrollY && goingUp) {
setGoingUp(false);
}
if (prevScrollY.current > currentScrollY && !goingUp) {
setGoingUp(true);
}
prevScrollY.current = currentScrollY;
console.log(goingUp, currentScrollY);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
return (
<div>
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
</div>
);
};
export default App;
https://codesandbox.io/s/react-setstate-from-event-listener-q7to8
In short, you need to add goingUp
as the dependency of useEffect.
如果你使用 []
,你只会 create/remove 一个带有函数的监听器(handleScroll
,它是在初始渲染中创建的)。换句话说,当 re-render 时,滚动事件侦听器仍在使用初始渲染中的旧 handleScroll
。
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
Using custom hooks
我建议将整个逻辑移动到自定义钩子中,这可以使您的代码更加清晰且易于重用。我使用 useRef
来存储以前的值。
export function useScrollDirection() {
const prevScrollY = useRef(0)
const [goingUp, setGoingUp] = useState(false);
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY.current < currentScrollY && goingUp) {
setGoingUp(false);
}
if (prevScrollY.current > currentScrollY && !goingUp) {
setGoingUp(true);
}
prevScrollY.current = currentScrollY;
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
return goingUp ? 'up' : 'down';
}
这是我针对这个案例的解决方案
const [pageUp, setPageUp] = useState(false)
const lastScroll = useRef(0)
const checkScrollTop = () => {
const currentScroll = window.pageYOffset
setPageUp(lastScroll.current > currentScroll)
lastScroll.current = currentScroll
}
// lodash throttle function
const throttledFunc = throttle(checkScrollTop, 400, { leading: true })
useEffect(() => {
window.addEventListener("scroll", throttledFunc, false)
return () => {
window.removeEventListener("scroll", throttledFunc)
}
}, [])
仅在 componentDidMount
时添加事件侦听器。并非在 pageUp
引起的所有变化中。所以,我们不需要添加 pageUp
到 useEffect
依赖项。
Hint:
您可以将 throttledFunc
添加到 useEffect
如果它发生变化并且您需要 re-render 组件
const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;
首先,对于 class 组件,这工作正常并且不会导致任何问题。
但是,在带有钩子的功能组件中,每当我尝试从我的滚动事件侦听器的函数 handleScroll
设置状态时,即使我使用了去抖动,我的状态也无法更新或应用程序的性能受到严重影响。
import React, { useState, useEffect } from "react";
import debounce from "debounce";
let prevScrollY = 0;
const App = () => {
const [goingUp, setGoingUp] = useState(false);
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY < currentScrollY && goingUp) {
debounce(() => {
setGoingUp(false);
}, 1000);
}
if (prevScrollY > currentScrollY && !goingUp) {
debounce(() => {
setGoingUp(true);
}, 1000);
}
prevScrollY = currentScrollY;
console.log(goingUp, currentScrollY);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div>
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
</div>
);
};
export default App;
尝试在 handleScroll
函数中使用 useCallback
钩子,但没有太大帮助。
我做错了什么?如何在不对性能产生巨大影响的情况下从 handleScroll
设置状态?
我已经针对这个问题创建了一个沙箱。
你在每个渲染器上都是 re-creating handleScroll
函数,所以它指的是实际数据 (goingup
) 所以你在这里不需要 useCallback
。
但是除了你重新创建 handleScroll
你被设置为事件侦听器只是它的第一个实例 - 因为你给 useEffect(..., [])
它 运行 只在挂载时一次。
一种解决方案是 re-subscribe 到 scroll
和 up-to-date handleScroll
。为此,您必须在 useEffect
中 return 清理函数(现在它 return 又订阅了一个),并在每次渲染时将 []
删除到 运行 :
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
PS 最好使用 passive event listener 滚动。
在你的代码中我发现了几个问题:
1) useEffect 中的 []
意味着它不会看到任何状态变化,例如 goingUp
的变化。它将始终看到 goingUp
2) debounce
这样不行。它 returns 一个新的去抖功能。
3) 通常全局变量是一个 anti-pattern,认为它只适用于您的情况。
4) 正如@skyboyer 所提到的,您的滚动侦听器不是被动的。
import React, { useState, useEffect, useRef } from "react";
const App = () => {
const prevScrollY = useRef(0);
const [goingUp, setGoingUp] = useState(false);
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY.current < currentScrollY && goingUp) {
setGoingUp(false);
}
if (prevScrollY.current > currentScrollY && !goingUp) {
setGoingUp(true);
}
prevScrollY.current = currentScrollY;
console.log(goingUp, currentScrollY);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
return (
<div>
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
<div style={{ background: "orange", height: 100, margin: 10 }} />
</div>
);
};
export default App;
https://codesandbox.io/s/react-setstate-from-event-listener-q7to8
In short, you need to add
goingUp
as the dependency of useEffect.
如果你使用 []
,你只会 create/remove 一个带有函数的监听器(handleScroll
,它是在初始渲染中创建的)。换句话说,当 re-render 时,滚动事件侦听器仍在使用初始渲染中的旧 handleScroll
。
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
Using custom hooks
我建议将整个逻辑移动到自定义钩子中,这可以使您的代码更加清晰且易于重用。我使用 useRef
来存储以前的值。
export function useScrollDirection() {
const prevScrollY = useRef(0)
const [goingUp, setGoingUp] = useState(false);
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (prevScrollY.current < currentScrollY && goingUp) {
setGoingUp(false);
}
if (prevScrollY.current > currentScrollY && !goingUp) {
setGoingUp(true);
}
prevScrollY.current = currentScrollY;
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [goingUp]);
return goingUp ? 'up' : 'down';
}
这是我针对这个案例的解决方案
const [pageUp, setPageUp] = useState(false)
const lastScroll = useRef(0)
const checkScrollTop = () => {
const currentScroll = window.pageYOffset
setPageUp(lastScroll.current > currentScroll)
lastScroll.current = currentScroll
}
// lodash throttle function
const throttledFunc = throttle(checkScrollTop, 400, { leading: true })
useEffect(() => {
window.addEventListener("scroll", throttledFunc, false)
return () => {
window.removeEventListener("scroll", throttledFunc)
}
}, [])
仅在 componentDidMount
时添加事件侦听器。并非在 pageUp
引起的所有变化中。所以,我们不需要添加 pageUp
到 useEffect
依赖项。
Hint:
您可以将 throttledFunc
添加到 useEffect
如果它发生变化并且您需要 re-render 组件
const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;