skip to Main Content

Code:

export const HomePage = (): JSX.Element => {
  const refContainer = useRef<HTMLDivElement>(null);
  const [scrollY, setScrollY] = useState<number>(0);
  const { current: elContainer } = refContainer;
  const handleScroll = useCallback(() => {
    if (elContainer) setScrollY(elContainer.scrollTop);
  }, []);
  useEffect(() => {
    document.addEventListener("scroll", handleScroll, { passive: true });
    return () => removeEventListener("scroll", handleScroll);
  }, [handleScroll]);
  return (
    <div className="pageScreen overflow-scroll" ref={refContainer}>
      <Works scrollY={scrollY} />
    </div>
  );
};

ScrollY state doesn’t change, because elContainer is null. How can i fix that? Thanks.

3

Answers


  1. The use of useCallback memoizes the scroll handler, causing it to reference a stale value of elContainer, particularly when it was null on the initial render.

    Additionally, an element’s scrollTop only changes when the element’s content itself is scrolled, not the whole document. As a result, the scroll event handler should be attached to the div, allowing you greatly simplify your code, like so:

    export const HomePage = (): JSX.Element => {
      const [scrollY, setScrollY] = useState<number>(0);
      
      const handleScroll = (evt) => setScrollY(evt.currentTarget.scrollTop);
    
      return (
        <div className="pageScreen overflow-scroll" onScroll={handleScroll}>
          <Works scrollY={scrollY} />
        </div>
      );
    };
    

    Just note that for the event to work, the div itself needs to be able scroll, not the document, so you also need to style your div with overflow: auto and a max-height.

    Here is a CodeSandbox demo of this in action.

    Login or Signup to reply.
  2. const { current: elContainer } = refContainer;
    

    You are extracting the .current property from the ref during the very first render, and saving it in a variable elContainer. This value is null, because nothing is on the page yet. And since you only set up the scroll event once, handle scroll is forever using this null value.

    Instead, don’t access .current until handleScroll is called. That way, the first render will already be complete, and refContainer.current will have been mutated to have the element:

      const handleScroll = useCallback(() => {
        const { current: elContainer } = refContainer; // Moved this inside the function
        if (elContainer) setScrollY(elContainer.scrollTop);
      }, []);
    
    Login or Signup to reply.
  3. This should work :

        import { FC, useCallback, useEffect, useRef, useState } from 'react'
    
        export const HomePage: FC = () => {
          const [scrollY, setScrollY] = useState(0)
          const refContainer = useRef<HTMLDivElement>(null)
    
          const handleScroll = useCallback(() => {
            if (refContainer.current) {
              setScrollY(-refContainer.current.getBoundingClientRect().y)
            }
          }, [])
    
          useEffect(() => {
            document.addEventListener('scroll', handleScroll)
    
            return () => {
              document.removeEventListener('scroll', handleScroll)
            }
          }, [handleScroll])
    
          return (
            <div className="pageScreen overflow-scroll" ref={refContainer}>
              <Works scrollY={scrollY} />
            </div>
          )
        }
    

    Overall, the proposed code is more concise, easier to read, and more reliable. It uses the FC type to define the component, which is a shorthand for FunctionComponent. It uses the getBoundingClientRect method to get the scroll position, which is more accurate than using scrollTop. And it uses document.removeEventListener to remove the event listener on unmount, which is more specific and less prone to errors.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search