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.