Reputation: 3755
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.
Upvotes: 24
Views: 49533
Reputation: 1
const objDiv = document.getElementById('chat_body');
objDiv.scrollTop = objDiv.scrollHeight;
Upvotes: -1
Reputation: 895
Here is my solution for this case
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)
}
}, [])
Adding event listeners should be once when componentDidMount
only. not in every changes causes by pageUp
. So, we don't need to add pageUp
to useEffect
dependencies.
Hint:
you can add throttledFunc
to useEffect
if it changes and you need to re-render the component
Upvotes: 1
Reputation: 368
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';
}
Upvotes: 9
Reputation: 12727
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
Upvotes: 25
Reputation: 23705
Your are re-creating handleScroll
function on each render so it's refers to actual data(goingup
) so you don't need useCallback
here.
But besides you recreate handleScroll
you are set as event listener only first instance of it - since you give useEffect(..., [])
it runs only once on mount.
The one solution is to re-subscribe to scroll
with up-to-date handleScroll
. To do that you have to return cleanup function within useEffect
(by now it returns one more subscribing) and remove []
to run it on each render:
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
PS and you better use passive event listener for scroll.
Upvotes: 3