React app does not fire off onScroll events as expected

I am trying to show a specific component – a “sticky header” piece when the page displaying it, is scrolled X number of points off the top.

I tried doing this with the useWindowScroll hook from the use-react package, as well with a short fragment below:

  const [scrollTop, setScrollTop] = useState(0);

  function setScroll() {
    console.log('setScroll');
    setScrollTop(window.pageYOffset);
    console.log(new Date().getTime());
  }

  useEffect(() => {
    function watchScroll() {
      console.log('watchScroll');
      window.addEventListener("scroll", setScroll);
    }
    watchScroll();
    return () => {
      window.removeEventListener("scroll", setScroll);
    };
  });

  console.log('scroll top:', scrollTop);

Ideally, we probably want to have a bit of a delay before each new scroll event is processed, yet it is not my problem.

I would expect setScroll to get called whenever I scroll, every however many millisecond. However, this is NOT happening. In fact, I don’t see it called ever.

Since it is not being called when approriate, my const shouldShowStickyHeader = scrollTop > 1000; is always set to false. Hence, my is never shown. I want it to appear when the scroll to top distance is 1,000.

What is the problem?

I also tried version with everything inside the effects hook as:

  const [scrollTop, setScrollTop] = useState(0);

  useEffect(() => {
    function setScroll() {
      console.log('setScroll');
      setScrollTop(window.pageYOffset);
      console.log(new Date().getTime());
    }
    window.addEventListener("scroll", setScroll);
    return () => {
      window.removeEventListener("scroll", setScroll);
    };
  }, [scrollTop]);

I don’t see setScroll being printed in the console. What could be the problem?

I did a little test with document.querySelectorAll("*").forEach(element => element.addEventListener("scroll", ({target}) => console.log(target, target?.id, target?.parent, target?.parent?.id)));. There are indeed events being fired off when I scroll. See:

<div data-testid="vdp-scroll-container" id="vdp-scroll-container" class="jsx-1959376998 vdp-scroll-container">
...
<div data-testid="vdp-scroll-container" id="vdp-scroll-container" class="jsx-1959376998 vdp-scroll-container">

I tried putting listers directly on this element with:

document.getElementById('#vdp-scroll-container')?.addEventListener("scroll", setScroll);
    return () => {
      document.getElementById('#vdp-scroll-container')?.removeEventListener("scroll", setScroll);
    };

Still, I don’t see anything getting fired off.

The styles on this <div/> are below:

height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow-y: scroll;

Am I not getting anything because of that?

Answer

You should pass an empty array to useEffect as the second argument so that the function is fired only after the component mounts for the first time. Not passing a second argument, will trigger the function after every rerender and passing [scrollTop] will trigger the function every time scrollTop changes.

Something like this should work:

  const [offset, setOffset] = React.useState(null);
  const setScroll = () => {
    setOffset(window.scrollY);
  };

  React.useEffect(() => {
    window.addEventListener("scroll", setScroll);
    return () => {
      window.removeEventListener("scroll", setScroll);
    };
  }, []);

Working example: https://codesandbox.io/s/zen-cdn-ll6es?file=/src/App.js