BlogReactJS

A ReactJS hook for listening to scroll direction

Written by Codemzy on February 9th, 2022

In this blog post, I share a ReactJS hook you can use to listen to the scroll direction of the browser. Is the user going up or down the page? Now you can always know! Unless they stay still!

Today I coded a pretty cool (I think) ReactJS hook that lets you know which direction a user is scrolling on the page.

The use case for this hook was to be able to show a header when a user scrolled up, but hide it when they scrolled down. I’m planning to write a more detailed blog post about the disappearing header, but I’ll share the hook first since there could be other uses for knowing scroll direction.

import { useState, useEffect } from 'react';

function useScrollDirection() {
    const [scrollDirection, setScrollDirection] = useState(null);

    useEffect(() => {
        let lastScrollY = window.pageYOffset;
        // function to run on scroll
        const updateScrollDirection = () => {
            const scrollY = window.pageYOffset;
            const direction = scrollY > lastScrollY ? "down" : "up";
            if (direction !== scrollDirection) {
              setScrollDirection(direction);
            }
            lastScrollY = scrollY > 0 ? scrollY : 0;
        };
        window.addEventListener("scroll", updateScrollDirection); // add event listener
        return () => {
            window.removeEventListener("scroll", updateScrollDirection); // clean up
        }
    }, [scrollDirection]); // run when scroll direction changes

    return scrollDirection;
};

That’s the hook… it took a lot less code than I expected! And here’s how you can use it if you are using the scroll direction to change the class of an element, for example:

function MyComponent(props) {
    const scrollDirection = useScrollDirection();

    return (
        <div className={`some-class ${ scrollDirection === "down" ? "down" : "up"}`} />
    );
};

If you are happy with the code feel free to copy and paste it into your ReactJS projects. Or, read on for some more details on how it works and a couple of tweaks you can make.

The hook

Let’s break the hook down and see how it works.

First, we are using Reacts useEffect. This is where any side effects should be in our React code. Adding an event listener, I would say that’s a side effect.

useEffect(() => {
    // ...
    window.addEventListener("scroll", updateScrollDirection); // add event listener
    return () => {
        window.removeEventListener("scroll", updateScrollDirection); // clean up
    }
}, [scrollDirection]); // run when scroll direction changes

The useEffect depends on scrollDirection, which is stored in state so that the component re-renders when the scroll direction changes.

The hook adds an event listener that listens to the scroll event. That makes sense since we want to know when the user scrolls!

When the scroll event happens, we want to know what direction the scroll goes in. And that’s what this function does.

let lastScrollY = window.pageYOffset; // the last scroll position
// function to run on scroll
const updateScrollDirection = () => {
    const scrollY = window.pageYOffset; // the new scroll position
    const direction = scrollY > lastScrollY ? "down" : "up"; // the direction of the scroll
    if (direction !== scrollDirection) {
      setScrollDirection(direction); // if the direction has changed update the state
    }
    lastScrollY = scrollY > 0 ? scrollY : 0;
};

To avoid adding multiple event listeners, we have a clean function to destroy the event listener before the useEffect runs again or when the component dismounts.

return () => {
    window.removeEventListener("scroll", updateScrollDirection); // clean up
}

The tweaks

Now there are a couple of tweaks you can make, depending on your use case.

The hook currently listens for the scroll event. And that’s probably created by the user but it might not be. If an element gets removed from the page and the page height changes, in a text editor, for example, the browser could scroll.

You might want to just listen to mouse scrolls and touch scrolls instead. For example wheel events and touchmove events. But these events might not cause a scroll. And the user could also scroll with the browser scroll bar or keyboard, so on balance, I decided to go with the more general scroll event instead.

And you might not want to listen to the smallest of scrolls. For example, if the user just slightly knocks the mouse wheel by accident. This could be annoying if your hook triggers some big event on the page.

You can add a bit of a buffer to prevent this like scrollY - lastScrollY > 10 || scrollY - lastScrollY < -10 to exclude tiny scroll events.

This will update the hook to:

function useScrollDirection() {
    const [scrollDirection, setScrollDirection] = useState(null);

    useEffect(() => {
        let lastScrollY = window.pageYOffset;
        // function to run on scroll
        const updateScrollDirection = () => {
            const scrollY = window.pageYOffset;
            const direction = scrollY > lastScrollY ? "down" : "up";
            if (direction !== scrollDirection && (scrollY - lastScrollY > 10 || scrollY - lastScrollY < -10)) {
              setScrollDirection(direction);
            }
            lastScrollY = scrollY > 0 ? scrollY : 0;
        };
        window.addEventListener("scroll", updateScrollDirection); // add event listener
        return () => {
            window.removeEventListener("scroll", updateScrollDirection); // clean up
        }
    }, [scrollDirection]); // run when scroll direction changes

    return scrollDirection;
};

In the next blog post, I'll cover how you can use the useScrollDirection hook to create a disappearing sticky header.