skip to Main Content

For some reason, when I acces ‘height’ or ‘scrollPos’ in the ‘handleScroll’ function, they have their initial value. However, if I add a ‘p’ tag with ‘{height}’, it is displayed correctly.

const [scrollPos, setScrollPos] = useState(0);
  const [height, setHeight] = useState(0);
  const [top, setTop] = useState(0);
  const ref = useRef(null);

  useEffect(() => {
    setHeight(ref?.current?.clientHeight);
    setScrollPos(-window.pageYOffset);
    window.addEventListener("scroll", handleScroll);
  }, []);

  const handleScroll = () => {
    const x = -window.pageYOffset;

    const dif = x - scrollPos;

    setTop(top + dif > 0 ? 0 : top + dif < -height ? -height : top + dif);
    setScrollPos(x);
  };

I am converting the component from a CC to a RFC, and it worked before.

2

Answers


  1. You can use a callback function as the second argument of the useState hook, which will receive the previous state value as a parameter, and return the new state value based on it. This way, you can avoid using stale state values in your event handler. For example:

    const handleScroll = () => {
      const x = -window.pageYOffset;
      setScrollPos((prevScrollPos) => {
        const dif = x - prevScrollPos;
        setTop((prevTop) =>
          prevTop + dif > 0 ? 0 : prevTop + dif < -height ? -height : prevTop + dif
        );
        return x;
      });
    };
    
    Login or Signup to reply.
  2. The problem comes from the fact that the state of handleScroll never changes when it’s triggered by the event. So the callback of the event needs to be recreated every time it is needed to have the current state updated inside the implementaiton of the callback.

    Here’s a solution:

    In the dependency array of the useEffect creating the event, pass the state variables to be used. And, to avoid the event to be duplicated every time the useEffect callback is called, it needs to be removed. For this you can use the return callback of the function

    Here what your new hooks will looks like with this solution:

      useEffect(() => {
        setHeight(ref?.current?.clientHeight);
        setScrollPos(-window.pageYOffset);
      }, []);
    
    
      useEffect(() => {
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
      }, [height, scrollPos, top]);
    

    That way, the event will be recreated everytime one of the three values is updated.

    Update

    As you commented, another way to do it is to pass directly the function handleScroll inside the dependencies array of the useEffect. But in that case I recommand you to use the useCallback hook that will recompute the function only if a value inside the dependencies array has changed (else it will be at each rerender of the component, and that happen a lot)

     const handleScroll = useCallback(() => {
        // ...
      }, [top, height, scrollPos]);
    
      useEffect(() => {
        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
      }, [handleScroll]);
    

    Why does this works ?

    I think the best way to explain this is to see the differents states of a component as snapshot. Each time a value change, a new snapshot is created, rewritting the implementation of every function with the values of the states, rerendering the template etc…

    So, everytime the state is updated, the implementation of the function of the component will be recomputed with the new values.
    Meaning that if i have this state in a component:

    const [count, setCount] = useState(0);
    

    And a function like this:

    function getCount(){
      return count; // Let's supposed that count is in th
    }
    

    Will actually be computed like this in the snapshot :

    function getCount(){
      return 0;
    }
    

    So, when you pass the handleScroll function as a callback of window.addEventListener, you are actually passing a function computed with a specific value of the state of the component, so the callback will never be up to date.

    That is way doing this method works, you are actually passing a different function everytime !

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