Detecting scroll direction
It can be detected by storing the previous scrollTop
value and comparing the current scrollTop value with it.
JavaScript :
var lastScrollTop = 0;
// element should be replaced with the actual target element on which you have applied scroll, use window in case of no target element.
element.addEventListener("scroll", function(){ // or window.addEventListener("scroll"....
var st = window.pageYOffset || document.documentElement.scrollTop; // Credits: "https://github.com/qeremy/so/blob/master/so.dom.js#L426"
if (st > lastScrollTop){
// downscroll code
} else {
// upscroll code
}
lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling
}, false);
Detect scroll direction in React js
This is because you defined a useEffect()
without any dependencies, so your useEffect()
will only run once, and it never calls handleNavigation()
on y
changes. To fix this you need to add y
to your dependency array to tell your useEffect()
run once the y
value gets changes. Then you need another change to take effect in your code, where you are trying to initialize your y
with window.scrollY
, so you should either, do this in your useState()
like:
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
If for some reason window may not be available there or you don't want to do it here, you can do it in two separate useEffect()
s.
So your useEffect()
s should be like this:
useEffect(() => {
setY(window.scrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
UPDATE (Working solutions)
After implementing this solution on my own. I found out there are some notes that should be applied to this solution. So since the handleNavigation()
will change y
value directly we can ignore the y
as our dependency and then add handleNavigation()
as a dependency to our useEffect()
, then due to this change we should optimize handleNavigation()
, so we should use useCallback()
for it. Then the final result will be something like this:
const [y, setY] = useState(window.scrollY);
const handleNavigation = useCallback(
e => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
}, [y]
);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation]);
After a comment from @RezaSam I noticed that I made a teeny tiny mistake in the memoized version. Where I call handleNavigation
within another arrow function, I found out (via the browser dev tool, event listeners tab) in each component rerender it will register a new event to the window
so it might ruin the whole thing up.
Working demo:
Final Optimized Solution
After all, I ended up that memoization in this case will help us to register a single event, to recognize scroll direction but it is not fully optimized in printing the consoles, because we are consoling inside the handleNavigation
function and there is no other way around to print the desired consoles in the current implementation.
So, I realized there is a better way of storing the last page scroll position each time we want to check for a new position. Also to get rid of a huge amount of consoling scrolling up and scrolling down, we should define a threshold (Use debounce approach) to trigger the scroll event change. So I just searched through the web a bit and ended up with this gist which was very useful. Then with the inspiration of it, I implement a simpler version.
This is how it looks:
const [scrollDir, setScrollDir] = useState("scrolling down");
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
How it works?
I will simply go from top to down and explain each block of code.
So I just defined a threshold point with the initial value of
0
then whenever the scroll goes up or down it will make the new calculation you can increase it if you don't want to immediately calculate new page offset.Then instead of using
scrollY
I decide to usepageYOffset
which is more reliable in cross browsing.In the
updateScrollDir
function, we will simply check if the threshold is met or not, then if it met I will specify the scroll direction based on the current and previous page offset.The most important part of it is the
onScroll
function. I just usedrequestAnimationFrame
to make sure that we are calculating the new offset after the page got rendered completely after scroll. And then withticking
flag, we will make sure we are just run our event listener callback once in eachrequestAnimationFrame
.At last, we defined our listener and our cleanup function.
Then the
scrollDir
state will contain the actual scroll direction.
Working demo:
Detecting scrolling direction on the page - updating prev value
Try the following. It should work since this way the previous value is stored in the instance of the component.
componentDidMount() {
this.prev = window.scrollY;
window.addEventListener('scroll', e => this.handleNavigation(e));
}
handleNavigation = (e) => {
const window = e.currentTarget;
if (this.prev > window.scrollY) {
console.log("scrolling up");
} else if (this.prev < window.scrollY) {
console.log("scrolling down");
}
this.prev = window.scrollY;
};
Javascript not detecting scroll direction
window.onscroll = function(e) {
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
scrollY <= this.lastScroll
? console.log('up')
: console.log('down');
this.lastScroll = scrollY ;
}
or instead of window, you can pass a reference to the element:
ContentSection.onscroll = function() {
var scrollY = ContentSection.scrollTop;
scrollY <= this.lastScroll
? console.log('up')
: console.log('down');
this.lastScroll = scrollY;
}
How can I determine the direction of a jQuery scroll event?
Check current scrollTop
vs previous scrollTop
var lastScrollTop = 0;
$(window).scroll(function(event){
var st = $(this).scrollTop();
if (st > lastScrollTop){
// downscroll code
} else {
// upscroll code
}
lastScrollTop = st;
});
Angular specific way to determine scrolling direction
Maybe I have lot more complex structure in my app, which includes dynamic content from various components, so I tried below and it worked seamlessly!
private scrollChangeCallback: () => void;
currentPosition: any;
startPosition: number;
ngAfterViewInit() {
this.scrollChangeCallback = () => this.onContentScrolled(event);
window.addEventListener('scroll', this.scrollChangeCallback, true);
}
onContentScrolled(e) {
this.startPosition = e.srcElement.scrollTop;
let scroll = e.srcElement.scrollTop;
if (scroll > this.currentPosition) {
this.showButton = false;
} else {
this.showButton = true;
}
this.currentPosition = scroll;
}
ngOnDestroy() {
window.removeEventListener('scroll', this.scrollChangeCallback, true);
}
e.srcElement works like a charm!
And thanks for all solutions above! They weren't wrong just didn't fit to my app
Detecting scroll direction using hooks
It's the nature of Reactjs's state, it's updated asynchronously
For your case, use useRef()
instead, so you won't need to care about the thing above:
const scollPosition = useRef(0);
const handleScroll = () => {
const position = window.pageYOffset;
console.log(position);
if (position > scollPosition.current) {
console.log("down");
} else {
console.log("up");
}
scollPosition.current = position;
};
useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
Working example:
function App() {
const scollPosition = React.useRef(0);
const handleScroll = () => {
const position = window.pageYOffset;
console.log(position);
if (position > scollPosition.current) {
console.log("down");
} else {
console.log("up");
}
scollPosition.current = position;
};
React.useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div>
<div id="root"></div>
Related Topics
How to Capture the Browser Window Close Event
Easiest Way to Mask Characters in Html(5) Text Input
How to Add Counter in Angular 6
How to Convert Image to Byte Array Using JavaScript Only to Store Image on SQL Server
How to Get Pasted Value from Reactjs Onpaste Event
Extracting Key:Value Pairs Assoc With Regex from String on JavaScript
How to Place Button on Right Side in Reactjs
Javascript Loop Wait Until API Call Finished
Google Firestore - How to Get Several Documents by Multiple Ids in One Round-Trip
Change Td Background Colour Based on the Value
Regex - Get All Characters After Last Slash in Url
Cors - Response to Preflight Request Doesn't Pass Access Control Check
Enable Utf-8 Encoding for JavaScript
Typescript: How to Take Subset of Attributes from an Object
Angular 4 - Cannot Read Property Status of Null While Displaying Validation Error
Combine Array of Objects With Similar Keys Conditionally Using JavaScript
Cannot Set Property ... Which Only Has Getter (Javascript Es6)