First of all, with a class component, this works fine and does not cause any issues.
However, in functional component with hooks, whenever I try to set state from my scroll event listener's function handleScroll
, my state fails to get updated or app's performance gets affected drastically even though I am using debounce.
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;
Tried to use useCallback
hook in handleScroll
function but it did not help much.
What am I doing wrong? How can I set state from handleScroll
without a huge impact on performance?
I've created a sandbox with this issue.
In your code I see several issues:
1) []
in useEffect means it will not see any changes of state, like changes of goingUp
. It will always see initial value of goingUp
2) debounce
does not work so. It returns a new debounced function.
3) usually global variables is an anti-pattern, thought it works just in your case.
4) your scroll listener is not passive, as mentioned by @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.
If you use []
, you will only create/remove a listener with a function(handleScroll
, which is created in the initial render). In other words, when re-render, the scroll event listener is still using the old handleScroll
from the initial render.
useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [goingUp]);
Using custom hooks
I recommend move the whole logic into a custom hooks, which can make your code more clear and easy to reuse. I use useRef
to store the previous value.
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'; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With