1
const Navbar = () => {
  const prevScrollY = React.useRef<number>();

  const [isHidden, setIsHidden] = React.useState(false);

  React.useEffect(() => {
    const onScroll = () => {
      const scrolledDown = window.scrollY > prevScrollY.current!;
      console.log(`is hidden ${isHidden}`);
      if (scrolledDown && !isHidden) {
        setIsHidden(true);
        console.log(`set hidden true`);
      } else if (!scrolledDown && isHidden) {
        console.log(`set hidden false. THIS NEVER HAPPENS`);
        setIsHidden(false);
      }

      prevScrollY.current = window.scrollY;
    };

    console.log("adding listener");

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

  return isHidden ? null : <div>navbar</div>;
};

Full example

The console.log(`is hidden ${isHidden}`); always prints false, and the setIsHidden(true) always gets triggered but never seems to change the state. Why? Basically the isHidden is never setto false, except after the useState initialization.

ZenVentzi
  • 3,945
  • 3
  • 33
  • 48

1 Answers1

4

Basically what happens is that your useEffect runs only twice on mount and on unmount (and that's apparently intentional), however the unwanted side-effect of this is that the value of isHidden that you're checking against in the onScroll method gets closured at it's initial value (which is false) - forever (until the unmount that is).

You could use functional form of the setter, where it receives the actual value of the state and put all the branching logic inside it. Something like:

  setIsHidden(isHidden => { // <- this will be the proper one
     const scrolledDown = window.scrollY > prevScrollY.current!;
     console.log(`is hidden ${isHidden}`);
     if (scrolledDown && !isHidden) {
        console.log(`set hidden true`);
        return true;  
      } else if (!scrolledDown && isHidden) {
        console.log(`set hidden false. THIS NEVER HAPPENS`);
        return false;
      } else {
        // ...

jayarjo
  • 16,124
  • 24
  • 94
  • 138
  • Thanks. But "else ..." else what? The thing is that whatever I return in the else the state is going to be updated, which it shouldn't. The goal is to update state only if the 2 if statements are true, otherwise, don't set new state. The solution above will always update state, on every single scroll. Which will also re-render the whole component. – ZenVentzi Mar 06 '19 at 11:47
  • 1
    To be honest, the way I've written my question, your first paragraph answers it perfectly. I created a [separate question](https://stackoverflow.com/questions/55023041/react-hooks-how-to-implement-usehideonscroll-hook), that extends on this one. So, just the first part of your answer should be sufficient. – ZenVentzi Mar 06 '19 at 12:29
  • 1
    I wasn't sure what exactly you were up to, so didn't complete the piece. I'm going to post a full snippet in your other question. – jayarjo Mar 07 '19 at 05:32