I have a website with different sections. I am using segment.io to track different actions on the page. How can I detect if a user has scrolled to the bottom of a div? I have tried the following but it seems to be triggered as soon as I scroll on the page and not when
I reached the bottom of the div.
componentDidMount() {
document.addEventListener('scroll', this.trackScrolling);
}
trackScrolling = () => {
const wrappedElement = document.getElementById('header');
if (wrappedElement.scrollHeight - wrappedElement.scrollTop === wrappedElement.clientHeight) {
console.log('header bottom reached');
document.removeEventListener('scroll', this.trackScrolling);
}
};
An even simpler way to do it is with scrollHeight, scrollTop, and clientHeight.
Subtract the scrolled height from the total scrollable height. If this is equal to the visible area, you've reached the bottom!
element.scrollHeight - element.scrollTop === element.clientHeight
In react, just add an onScroll listener to the scrollable element, and use event.target in the callback.
class Scrollable extends Component {
handleScroll = (e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom) { ... }
}
render() {
return (
<ScrollableElement onScroll={this.handleScroll}>
<OverflowingContent />
</ScrollableElement>
);
}
}
I found this to be more intuitive because it deals with the scrollable element itself, not the window, and it follows the normal React way of doing things (not using ids, ignoring DOM nodes).
You can also manipulate the equation to trigger higher up the page (lazy loading content/infinite scroll, for example).
you can use el.getBoundingClientRect().bottom to check if the bottom has been viewed
isBottom(el) {
return el.getBoundingClientRect().bottom <= window.innerHeight;
}
componentDidMount() {
document.addEventListener('scroll', this.trackScrolling);
}
componentWillUnmount() {
document.removeEventListener('scroll', this.trackScrolling);
}
trackScrolling = () => {
const wrappedElement = document.getElementById('header');
if (this.isBottom(wrappedElement)) {
console.log('header bottom reached');
document.removeEventListener('scroll', this.trackScrolling);
}
};
Here's a solution using React Hooks and ES6:
import React, { useRef, useEffect } from 'react';
const MyListComponent = () => {
const listInnerRef = useRef();
const onScroll = () => {
if (listInnerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = listInnerRef.current;
if (scrollTop + clientHeight === scrollHeight) {
// TO SOMETHING HERE
console.log('Reached bottom')
}
}
};
return (
<div className="list">
<div className="list-inner" onScroll={() => onScroll()} ref={listInnerRef}>
{/* List items */}
</div>
</div>
);
};
export default List;
This answer belongs to Brendan, let's make it functional
export default () => {
const handleScroll = (e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom) {
console.log("bottom")
}
}
return (
<div onScroll={handleScroll} style={{overflowY: 'scroll', maxHeight: '400px'}} >
//overflowing elements here
</div>
)
}
If the first div is not scrollable it won't work and onScroll didn't work for me in a child element like div after the first div so onScroll should be at the first HTML tag that has an overflow
We can also detect div's scroll end by using ref.
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
import styles from 'style.scss';
class Gallery extends Component{
paneDidMount = (node) => {
if(node) {
node.addEventListener("scroll", this.handleScroll.bind(this));
}
}
handleScroll = (event) => {
var node = event.target;
const bottom = node.scrollHeight - node.scrollTop === node.clientHeight;
if (bottom) {
console.log("BOTTOM REACHED:",bottom);
}
}
render() {
var that = this;
return(<div className={styles.gallery}>
<div ref={that.paneDidMount} className={styles.galleryContainer}>
...
</div>
</div>);
}
}
export default withRouter(Gallery);
Extending chandresh's answer to use react hooks and ref I would do it like this;
import React, {useState, useEffect} from 'react';
export default function Scrollable() {
const [referenceNode, setReferenceNode] = useState();
const [listItems] = useState(Array.from(Array(30).keys(), (n) => n + 1));
useEffect(() => {
return () => referenceNode.removeEventListener('scroll', handleScroll);
}, []);
function handleScroll(event) {
var node = event.target;
const bottom = node.scrollHeight - node.scrollTop === node.clientHeight;
if (bottom) {
console.log('BOTTOM REACHED:', bottom);
}
}
const paneDidMount = (node) => {
if (node) {
node.addEventListener('scroll', handleScroll);
setReferenceNode(node);
}
};
return (
<div
ref={paneDidMount}
style={{overflowY: 'scroll', maxHeight: '400px'}}
>
<ul>
{listItems.map((listItem) => <li>List Item {listItem}</li>)}
</ul>
</div>
);
}
Add following functions in your React.Component and you're done :]
componentDidMount() {
window.addEventListener("scroll", this.onScroll, false);
}
componentWillUnmount() {
window.removeEventListener("scroll", this.onScroll, false);
}
onScroll = () => {
if (this.hasReachedBottom()) {
this.props.onScrollToBottom();
}
};
hasReachedBottom() {
return (
document.body.offsetHeight + document.body.scrollTop ===
document.body.scrollHeight
);
}
I know this has already been answered but, I think another good solution is to use what's already available out in the open source community instead of DIY. React Waypoints is a library that exists to solve this very problem. (Though don't ask me why the this problem space of determining if a person scrolls past an HTML element is called "waypoints," haha)
I think it's very well designed with its props contract and definitely encourage you to check it out.
I used follow in my code
.modify-table-wrap {
padding-top: 50px;
height: 100%;
overflow-y: scroll;
}
And add code in target js
handleScroll = (event) => {
const { limit, offset } = this.state
const target = event.target
if (target.scrollHeight - target.scrollTop === target.clientHeight) {
this.setState({ offset: offset + limit }, this.fetchAPI)
}
}
return (
<div className="modify-table-wrap" onScroll={this.handleScroll}>
...
<div>
)
Put a div with 0 height after your scrolling div. then use this custom hooks to detect if this div is visible.
const bottomRef = useRef();
const reachedBottom = useCustomHooks(bottomRef);
return(
<div>
{search resault}
</div>
<div ref={bottomRef}/> )
reachedBottom will toggle to true if you reach bottom
To evaluate whether my browser has scrolled to the bottom of a div, I settled with this solution:
const el = document.querySelector('.your-element');
const atBottom = Math.ceil(el.scrollTop + el.offsetHeight) === el.scrollHeight;
The solution below works fine on most of browsers but has problem with some of them.
element.scrollHeight - element.scrollTop === element.clientHeight
The better and most accurate is to use the code below which works on all browsers.
Math.abs(e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop) < 1
So the final code should be something like this
const App = () => {
const handleScroll = (e) => {
const bottom = Math.abs(e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop) < 1;
if (bottom) { ... }
}
return(
<div onScroll={handleScroll}>
...
</div>
)
}
This answer belongs to Brendan, but I am able to use that code in this way.
window.addEventListener("scroll", (e) => {
const bottom =
e.target.scrollingElement.scrollHeight -
e.target.scrollingElement.scrollTop ===
e.target.scrollingElement.clientHeight;
console.log(e);
console.log(bottom);
if (bottom) {
console.log("Reached bottom");
}
});
While others are able to access directly inside target by
e.target.scrollHeight, I am able to achieve same by
e.target.scrollingElement.scrollHeight
Related
I'm using the infinite scrolling for my react app and have this function that detects when I'm exactly at the bottom of the page:
const [isFetching, setIsFetching] = useState(false);
// Fire Upon Reaching the Bottom of the Page
const handleScroll = () => {
if (
window.innerHeight +
Math.max(
window.pageYOffset,
document.documentElement.scrollTop,
document.body.scrollTop
) !==
document.documentElement.offsetHeight
)
return;
setIsFetching(true);
};
// Debounce the Scroll Event Function and Cancel it When Called
const debounceHandleScroll = debounce(handleScroll, 100);
useEffect(() => {
window.addEventListener("scroll", debounceHandleScroll);
return () => window.removeEventListener("scroll", debounceHandleScroll);
}, [debounceHandleScroll]);
debounceHandleScroll.cancel();
The problem lies when I load my page in my phone and it seems that because of the tab or bar of the mobile browser, it's not detecting the bottom of the page and so the content doesn't load.
Is there any way I can detect that the user is near the bottom and fire that function only then?
I think an IntersectionObserver could be what you're looking for.
You can check this tutorial for for basic information: https://dev.to/producthackers/intersection-observer-using-react-49ko
You could also turn this into a custom hook which takes a ref (in your case, a n element at the bottom of you page):
import { useEffect, useState } from 'react';
const useIsVisible = ref => {
const [isIntersecting, setIntersecting] = useState(false);
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting),
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, []);
return isIntersecting;
};
export default useIsVisible;
Maybe also the following package might help you, it makes implementing infinity scroll quite easy:
https://www.npmjs.com/package/react-infinite-scroll-component
I managed to changed the scroll function into this and now it's working.
const handleScroll = () => {
if (
window.innerHeight +
Math.max(
window.pageYOffset,
document.documentElement.scrollTop,
document.body.scrollTop
) >
document.documentElement.offsetHeight - 100
) {
setIsFetching(true);
} else {
return;
}
};
The number that's being reduced from document.documentElement.offsetHeight determines the amount remaining from the bottom of the page. 100 seems enough for me since I tested it on my phone and it works.
I am trying to select some value preferably px of how much I have scrolled down so I can conditionally hide the element.
Something like total height - scrolled height would be ideal
Problem
I'm having trouble selecting the proper property.
console.log doesn't help as it renders the actual body tag then.
Here's the code
const scrollHandler = (event) => {
let scrollTop = event.srcElement.body.offsetHeight;
console.log(scrollTop)
setIsSearchVisible(false)
}
useEffect(() => {
window.addEventListener('scroll', scrollHandler, true);
return () => {
window.removeEventListener('scroll', scrollHandler, true);
}
},[])
Would also appreciate it if someone could point me to the documentation of the same thanks!
I was able to figure it out , Instead of using the event object I simply used the window object, Something like this
const scrollHandler = (event) => {
let scrollTop = window.scrollY;
console.log(scrollTop);
setIsSearchVisible(false);
};
useEffect(() => {
window.addEventListener("scroll", scrollHandler, true);
return () => {
window.removeEventListener("scroll", scrollHandler, true);
};
}, []);
I created a responsive sidebar, the logic is implemented as follows, when the screen reaches below 765 pixels the sidebar is automatically hidden, but the problem is that when I refresh the page which is below 765 pixels the sidebar is displayed it looks like this
My code looks like this
function SideBar(props) {
const {someValue} = useContext(SideBarContext);
const {SideBarValue, SideBarWallpaperValue} = React.useContext(CounterContext);
const [SideBarThemeValue] = SideBarValue;
const [SideBarBackgroundValue] = SideBarWallpaperValue;
const [sideBarOpen, setSideBarOpen] = useState(true);
const [SideBarButtonContainer, setSideBarButtonContainer] = useState(false);
useEffect(() => {
window.addEventListener("resize", resize);
})
const resize = () => {
if(window.innerWidth < 765) {
setSideBarOpen(false)
setSideBarButtonContainer(true)
} else {
setSideBarOpen(true)
setSideBarButtonContainer(false)
}
}
const showSideBar = () => {
setSideBarOpen(!sideBarOpen)
}
return (
<>
{
SideBarButtonContainer ? <div className={"showSideBarButtonContainer"}>
<img className={"showSideBarButton"} onClick={() => showSideBar()} src={SideBarMenuIcon} alt={"Open"} />
</div> : null
}
<Menu isOpen={sideBarOpen}>
</Menu>
</>
);
}
I assume that when I refresh the page the sideBarOpen value becomes true, although I did a check inside the resize method and notice when I start to shrink the screen the sidebar disappears it looks like this
Try using useLayoutEffect to do push some state changes before actually rendering to screen.
useLayoutEffect(() => {
if (window.innerWidth < 765) {
setSideBarOpen(false);
setSideBarButtonContainer(true);
}
window.addEventListener("resize", resize);
}, [])
The default state of your sidebar is open, however you must calculate the initial state based on the width. Also you must only initialise the listener on window resize on initial render.
const [sideBarOpen, setSideBarOpen] = useState(() => window.innerWidth > 765);
const [SideBarButtonContainer, setSideBarButtonContainer] = useState(() => window.innerWidth < 765);
useEffect(() => {
window.addEventListener("resize", resize);
}, []); // Only initialize listener on initial render
const resize = () => {
if(window.innerWidth < 765) {
setSideBarOpen(false)
setSideBarButtonContainer(true)
} else {
setSideBarOpen(true)
setSideBarButtonContainer(false)
}
}
I would like to implement something like this using React hooks:
const header = document.querySelector(".nav-header");
function stickyHeader() {
if (window.pageYOffset > 600) {
header.classList.remove("header");
} else {
header.classList.add("header");
}
}
window.addEventListener("scroll", () => {
stickyHeader();
});
Above, is how I would have manipulated the DOM in vanilla js. I would like to do the same for a component in react.
Possibly try a getClassName method which returns a joined array of classes on every render
For a functional component
const getClassName = () => {
let classes = ["nav"];
if (window.pageYOffset > 600) {
classes.push("header");
}
//more conditions if required
return classes.join(" ");
//returns "nav header" || "nav"
}
and then in your component return method
return (
<div className={getClassName()}><div>
)
className is an element attribute. Therefore you need to set it in your render component function like below:
...
render() {
let className = 'menu';
if (this.props.isActive) {
className += ' menu-active';
}
return <span className={className}>Menu</span>
}
here is an example on how to handle scroll events with React
I am trying to figure out how to implement window.addEventListener in React. I'm developing a website with Gatsby and in "development" environment it works but whenever I start in production it gets an error. This is my code:
const checkHeader = () => {
// Detect scroll position
let viewportWidth = window.innerWidth || document.documentElement.clientWidth;
if (viewportWidth > 1100) {
let scrollPosition = Math.round(window.scrollY);
if (scrollPosition > 100){
document.querySelector('#nav').classList.add(`${headerStyles.sticky}`);
}
else {
document.querySelector('#nav').classList.remove(`${headerStyles.sticky}`);
}
} else {
}
};
// Run the checkHeader function every time you scroll
window.addEventListener('scroll', checkHeader);
I want to apply a class when Scroll. I've checked that I can't use "window." in React. How is the way to implement this code in React?
During development, react components are only run in the browser where window is defined. When building, Gatsby renders these components on the server where window is not defined.
Generally with React, the solution is to only access window in componentDidMount or to check that window exists before accessing it.
const checkHeader = () => {
// Detect scroll position
let viewportWidth = window.innerWidth || document.documentElement.clientWidth;
if (viewportWidth > 1100) {
let scrollPosition = Math.round(window.scrollY);
if (scrollPosition > 100){
document.querySelector('#nav').classList.add(`${headerStyles.sticky}`);
}
else {
document.querySelector('#nav').classList.remove(`${headerStyles.sticky}`);
}
} else {
}
};
// Check that window exists before accessing it
if (typeof window !== 'undefined') {
// Run the checkHeader function every time you scroll
window.addEventListener('scroll', checkHeader);
}
You can directly add window events with Gatsby since it performs server side rendering. To do that you need to add you listeners in gatsby-browser.js inside onClientEntry method which is called when client is loaded
// gatsby-browser.js
// ES6
const checkHeader = () => {
// Detect scroll position
let viewportWidth = window.innerWidth || document.documentElement.clientWidth;
if (viewportWidth > 1100) {
let scrollPosition = Math.round(window.scrollY);
if (scrollPosition > 100){
document.querySelector('#nav').classList.add(`${headerStyles.sticky}`);
}
else {
document.querySelector('#nav').classList.remove(`${headerStyles.sticky}`);
}
} else {
}
};
export const onClientEntry = () => {
// Run the checkHeader function every time you scroll
window.addEventListener('scroll', checkHeader);
}
Calling the function on did mount of the root component might solve your issue. for example:
// your entry component
const App = () => {
React.useEffect(() => { // Will be called after mounting the componnent.
onClientEntry();
}, []);
return (
<Home />
);
}
Hope this helps