2
const shouldHide = useHideOnScroll();
return shouldHide ? null : <div>something</div>

The useHideOnScroll behaviour should return updated value not on every scroll but only when there is a change.

The pseudo logic being something like the following:

if (scrolledDown && !isHidden) {
        setIsHidden(true);
      } else if (scrolledUp && isHidden) {
        setIsHidden(false);
      }

In words, if scroll down and not hidden, then hide. If scroll up and hidden, then unhide. But if scroll down and hidden, do nothing or scroll up and not hidden, do nothing.

How do you implement that with hooks?

ZenVentzi
  • 3,945
  • 3
  • 33
  • 48

4 Answers4

3

Here:

const useHideOnScroll = () => {
  const prevScrollY = React.useRef<number>();
  const [isHidden, setIsHidden] = React.useState(false);

  React.useEffect(() => {
    const onScroll = () => {
      setIsHidden(isHidden => {
        const scrolledDown = window.scrollY > prevScrollY.current!;
        if (scrolledDown && !isHidden) {
          return true;
        } else if (!scrolledDown && isHidden) {
          return false;
        } else {
          prevScrollY.current = window.scrollY;
          return isHidden;
        }
      });
    };

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

  return isHidden;
};

const Navbar = () => {
  const isHidden = useHideOnScroll();
  console.info("rerender");
  return isHidden ? null : <div className="navbar">navbar</div>;
};

export default Navbar;

You might have concern about setIsHidden causing rerender on every onScroll, by always returning some new state value, but a setter from useState is smart enough to update only if the value has actually changed.

Also your .navbar (I've added a class to it) shouldn't change the layout when it appears or your snippet will get locked in an infinite loop. Here're appropriate styles for it as well:

.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 30px;
  background: rgba(255, 255, 255, 0.8);
}

Full CodeSandbox: https://codesandbox.io/s/13kr4xqrwq

jayarjo
  • 16,124
  • 24
  • 94
  • 138
  • Thank you. Just to clarify, in the else statement, where we simply return isHidden does it make the component re-render or not? My guess is not since it sees that state hasn't changed. Is that correct? This is absolutely not an issue, but I'm curious why the example code `console.log('render')` twice on state change? – ZenVentzi Mar 07 '19 at 06:01
  • Replacing with ``console.info(`rerender ${isHidden}`);`` we get output such as ``rerender true rerender true rerender false rerender false `` instead of just once. Why is that? Again, this is not an issue but I'm trying to make sense of it all – ZenVentzi Mar 07 '19 at 06:09
  • Double thing is kind of mystery for me at the moment. I will read the source when I have spare time to see what is actually happening. At the moment you can debounce the `onScroll` handler (since it fires multiple times and I guess that's not desirable anyway). – jayarjo Mar 07 '19 at 07:06
3

Using hooks in React(16.8.0+)


import React, { useState, useEffect } from 'react'

function getWindowDistance() {
    const { pageYOffset: vertical, pageXOffset: horizontal } = window
    return {
        vertical,
        horizontal,
    }
}

export default function useWindowDistance() {
    const [windowDistance, setWindowDistance] = useState(getWindowDistance())

    useEffect(() => {
        function handleScroll() {
            setWindowDistance(getWindowDistance())
        }

        window.addEventListener('scroll', handleScroll)
        return () => window.removeEventListener('scroll', handleScroll)
    }, [])

    return windowDistance
}
2

You need to use window.addEventListener and https://reactjs.org/docs/hooks-custom.html guide.

That is my working example:

import React, { useState, useEffect } from "react";

const useHideOnScrolled = () => {
  const [hidden, setHidden] = useState(false);

  const handleScroll = () => {
    const top = window.pageYOffset || document.documentElement.scrollTop;
    setHidden(top !== 0);
  };

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

  return hidden;
};

export default useHideOnScrolled;

live demo: https://codesandbox.io/s/w0p3xkoq2l?fontsize=14

and i think name useIsScrolled() or something like that would be better

r g
  • 3,586
  • 1
  • 11
  • 27
  • Appreciate the time taken to answer! I don't think that it's complete though. Your solution simply works only at the top of the page, which is not the goal. The goal is to work all the time, no matter how much the user has scrolled. – ZenVentzi Mar 06 '19 at 15:21
  • You can check jayarjo's answer. While not 100% optimal, works as expected – ZenVentzi Mar 07 '19 at 08:34
  • @ZenVentzi oh, i get it. I'll try again then now and edit my answer – r g Mar 07 '19 at 08:50
1

After hours of dangling, here is what I came up with.

const useHideOnScroll = () => {
  const [isHidden, setIsHidden] = useState(false);
  const prevScrollY = useRef<number>();

  useEffect(() => {
    const onScroll = () => {
      const scrolledDown = window.scrollY > prevScrollY.current!;
      const scrolledUp = !scrolledDown;

      if (scrolledDown && !isHidden) {
        setIsHidden(true);
      } else if (scrolledUp && isHidden) {
        setIsHidden(false);
      }

      prevScrollY.current = window.scrollY;
    };

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

  return isHidden;
};

Usage:

const shouldHide = useHideOnScroll();
return shouldHide ? null : <div>something</div>

It's still suboptimal, because we reassign the onScroll when the isHidden changes. Everything else felt too hacky and undocumented. I'm really interested in finding a way to do the same, without reassigning onScroll. Comment if you know a way :)

ZenVentzi
  • 3,945
  • 3
  • 33
  • 48
  • You can move onScroll up with isHidden as parameter so you wont have to reassign it every time – r g Mar 07 '19 at 08:38