How do you detect scroll direction using React hooks? Let’s find out in this new post.
In the last year I migrated this blog on GatsbyJS. This basically means that now this blog is powered by React, a framework I love. In particular, I used only hooks and functional components. One of the thing I had to do was to detect the scroll direction in order to implement the sticky header with hide on scroll feature that you can find at the top of this page (and everywhere on this website). How can we do this? Searching on the web I found this well written post on stackoverflow that contains a raw implementation of what I needed. In this blog post I will show you how I evolved that react hook into into a production ready one that you can use to detect the scroll direction in a page.
Implementation
Let’s start from defining an enum that will represent the scroll direction. Let’s define it as string, because you know that enums in typescript are dangerous if you don’t define explicit values .
export enum ScrollDirection {
up = “up”,
down = “down”,
}
Now we can start to create our hook. We will define it as useScrollDirection
function and we will define two thing:
- a
threshold
variable that will be use as threshold: we will detect change in the scroll direction if the user scrolls for at least100
pixels up or down - a
scrollDir
react state defined with theuseState
hook that will store the current scroll direction. This is what we will return as result from our hook.
After this we will use the useEffect
hooks, an hook used to perform side effects in functional components. Inside it we will define all the scroll detection logic. The useEffect
hook is invoked without dependencies (the empty array []
passed as second parameter). As you may already know this basically means that its behavior will be equivalent to the componentDidMount
react class component lifecycle method. This is what we came up so far.
import { useEffect, useState } from "react";
export enum ScrollDirection {
up = “up”,
down = “down”,
}
export const useScrollDirection = () => {
const threshold = 100;
const [scrollDir, setScrollDir] = useState(ScrollDirection.up);
useEffect(() => {
// …scroll detection logic
}, []);
return scrollDir;
};
Now we are ready to write our scroll detection logic. The first thing to do is to save the initial Y scroll position of the window when we execute the hook. We will store in the previousScrollYPosition
variable. Then we will write some functions:
scrolledMoreThanThreshold
is a function that will detect is the user scroll more pixel than the ones defined in thethreshold
variable we defined before. To do this it will basically check that the absolute difference (no sign) between thepreviousScrollYPosition
and the current scroll position received as parameter is greater thanthreshold
isScrollingUp
is one of the two core function needed in order to detect the scroll direction. It basically check if the current scroll position is greater that the previous scroll position. We need also to define some additional checks needed in order to to avoid to receive false positive scroll direction (they will basically avoid scroll direction updates if the user is at the top/bottom of the page and it continues to scroll).updateScrollDirection
is the other core function. It basically uses the previous defined functions to check if the user is scrolling more that the threshold and the scroll direction (using theisScrollingUp
function). It then update thescrollDir
state and thepreviousScrollYPosition
variable if a scroll change has been detected.onScroll
is a function that attaches theupdateScrollDirection
function to thewindow.requestAnimationFrame
function so that we can calculate our scroll direction in sync with the window refresh frame rate (so if you have a refresh of 60 fps you will receive 60 calls per seconds). In this way we will be sure that our scroll direction calculation will happen after each page render/repaint.
Now we have all the functions we need in order to detect the scroll direction.
The last thing we need to do is to attach the onScroll
function to the scroll listener with the window.addEventListener("scroll", onScroll);
instruction.
Wait…how do we clean/remove the listener? 🤔 Well, the useEffect
hooks can return a function that will be used to cleanup every resource used in the hook when the component that launched it will be unmounted.
So the (real 🙇) last thing we need to do is to return the clean up function that will basically call window.removeEventListener("scroll", onScroll)
to remove the listener.
That’s it!!! 🚀🚀🚀 Below you can find the complete hook that you can copy/paste in your application. If you want you can try to go further by parametrizing some of the vaues used inside the hook (eg.: threshold)
import { useEffect, useState } from "react";
export enum ScrollDirection {
up = “up”,
down = “down”,
}
export const useScrollDirection = () => {
const threshold = 100;
const [scrollDir, setScrollDir] = useState(ScrollDirection.up);
useEffect(() => {
let previousScrollYPosition = window.scrollY;
const scrolledMoreThanThreshold = (currentScrollYPosition: number) =>
Math.abs(currentScrollYPosition - previousScrollYPosition) > threshold;
const isScrollingUp = (currentScrollYPosition: number) =>
currentScrollYPosition > previousScrollYPosition &&
!(previousScrollYPosition > 0 && currentScrollYPosition === 0) &&
!(currentScrollYPosition > 0 && previousScrollYPosition === 0);
const updateScrollDirection = () => {
const currentScrollYPosition = window.scrollY;
if (scrolledMoreThanThreshold(currentScrollYPosition)) {
const newScrollDirection = isScrollingUp(currentScrollYPosition)
? ScrollDirection.down
: ScrollDirection.up;
setScrollDir(newScrollDirection);
previousScrollYPosition =
currentScrollYPosition > 0 ? currentScrollYPosition : 0;
}
};
const onScroll = () => window.requestAnimationFrame(updateScrollDirection);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
return scrollDir;
};
Conclusion
Did you enjoy reading my post? If so, support my work by 💞 starring/funding some of my open source projects 💞.