ReactJS: How to update state while scrolling page - reactjs

I have created a function to detect scroll status, means if the user has scrolled to the bottom of the page then 'console.log(true)' and setting state. The function name is handleScroll and I am calling that function from helper file. And in my view file, I'm calling event listener to detect scroll change using the handleScroll function inside componentDidMount & later removing event listener by unmounting.
However, when I run the code initially state is set inside 'atBottom: false'. But later if I scroll down the page the function is not called again and I can't detect whether I am bottom of the page or not.
----> View file
import { handleScroll } from 'components/Helper.jsx'
class ScrollStatus extends Component {
constructor(props) {
super(props);
this.state = {
height: window.innerHeight,
scrollBottomStatus: false,
}
}
componentDidMount() {
window.addEventListener("scroll", handleScroll(this,
this.stateHandler));
}
componentWillUnmount() {
window.removeEventListener("scroll", handleScroll(this,
this.stateHandler));
}
stateHandler = (state) => {
this.setState(state);
}
render() {
return ( <div> Long text ... </div> ) }
}
export default ScrollStatus
----> helper file
export const handleScroll = (obj, stateHandler) => {
const windowHeight = "innerHeight" in window ? window.innerHeight :
document.documentElement.offsetHeight;
const body = document.body;
const html = document.documentElement;
const docHeight = Math.max(body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);
const windowBottom = Math.round(windowHeight + window.pageYOffset);
if (windowBottom >= docHeight) {
console.log(true)
stateHandler({
scrollBottomStatus: true
});
} else {
console.log(false)
stateHandler({
scrollBottomStatus: false
});
}
}
I want the function to keeping checking window height as I scroll down or up and keep updating the state 'isBottom' while scrolling.
I would appreciate the help.

When I check scrolling I always add a throttle (via lodash or ...) to throttle down the actions.
What I would do in your case.
1. Add eventlistener on mount, also remove on unmount.
componentDidMount = () => {
window.addEventListener('scroll', () => _.throttle(this.handleScroll, 100));
}
2. In the same component I'd handle the state update.
handleScroll = () => {
let scrollY = window.pageYOffset;
if(scrollY < 100) { this.setState({ // BLA })
}

Related

How to know the scrollbar reached top of the component in reactjs

I have got Component with scroll bar inside it. I would like to know when the scroll bar reaches the top of the component.
Please if anybody can guide me...
Created an example for you on codesandbox
Simplified example:
function Component() {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
const handleScroll = (e) => {
if (element.scrollTop === 0) {
console.log("do something");
// do whatever you want here
}
};
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
}, []);
return (
<div ref={ref}></div>
);
}
You can also make a hook out of it if you want to.
You can use the scroll event. No need for ref.
class MyComponent extends React.Component {
handleScroll=(evt)=> { // use this of object instance
if(!evt.currentTarget.scrollTop) {
}
}
render() {
return '<div onScroll={this.handleScroll}></div>';
}
}

React scroll event that has access to State

I'm trying to build an infinite scroll component in React (specifically using NextJS). I am having trouble with this feature because when I set a scroll event on the window, it doesn't have access to updated state. How can I write a scroll event that listens to any scrolling on the entire window that also has access to state like router query params?
Here's some code to see what I'm trying to do:
useEffect(() => {
window.addEventListener('scroll', handleScroll);
},[]);
const handleScroll = () => {
const el = infiniteScroll.current;
if (el) {
const rect = el.getBoundingClientRect();
const isVisible =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth);
if (isVisible && !isComplete && !isFetching) {
nextPage();
}
}
};
const nextPage = () => {
const params = router.query as any; // <------ these params here never update with state and are locked in to the the values they were at when the component mounted
params.page = params.page
? (parseInt((params as any).page) + 1).toString()
: '1';
router.replace(router, undefined, { scroll: false });
};
The issue is that the router value is locked at the place it was when the component mounted.
I've tried removing the empty array of dependencies for the useEffect at the top, but as you can imagine, this creates multiple scroll listeners and my events fire too many times. I've tried removing the eventListener before adding it every time, but it still fires too many times.
Every example I've found online seems to not need access to state variables, so they write code just like this and it works for them.
Any ideas how I can implement this?
I've tried to use the onScroll event, but it doesn't work unless you have a fixed height on the container so that you can use overflow-y: scroll.
You can use a ref to access and modify your state in the scope of the handleScroll function.
Here is how:
const yourRef = useRef('foo');
useEffect(() => {
const handleScroll = () => {
const value = yourRef.current;
if (value === 'foo') {
yourRef.current = 'bar'
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
I figured something out that works. Posting in case anyone else is having the same issue.
I created a custom hook called useScrollPosition that sets a listener on the window and updates the scroll position. It looks like this:
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
updatePosition();
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
and using that in my component like this:
useEffect(() => {
handleScroll();
}, [scrollPosition]);
allows me to access the current state of the router

React Hooks How to get to componentWillUnmount

Hello I'm trying to pass the following code to reacthooks:
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
class SomeComponent extends React.Component {
// 2. Initialise your ref and targetElement here
targetRef = React.createRef();
targetElement = null;
componentDidMount() {
// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
this.targetElement = this.targetRef.current;
}
showTargetElement = () => {
// ... some logic to show target element
// 4. Disable body scroll
disableBodyScroll(this.targetElement);
};
hideTargetElement = () => {
// ... some logic to hide target element
// 5. Re-enable body scroll
enableBodyScroll(this.targetElement);
}
componentWillUnmount() {
// 5. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
// OR useful for if the `hideTargetElement()` function got circumvented eg. visitor
// clicks a link which takes him/her to a different page within the app.
clearAllBodyScrollLocks();
}
render() {
return (
// 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
<SomeOtherComponent ref={this.targetRef}>
some JSX to go here
</SomeOtherComponent>
);
}
}
And then I did the following with hooks:
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const targetRef = useRef();
const showTargetElement = () => {
disableBodyScroll(targetRef);
};
const hideTargetElement = () => {
enableBodyScroll(targetRef);
};
useEffect(() => {
if (modalIsOpen === true) {
showTargetElement();
} else {
hideTargetElement();
}
}, [modalIsOpen]);
I don't know if I did it correctly with useRef and useEffect, but it worked, but I can't imagine how I'm going to get to my componentWillUnmount to call mine:
clearAllBodyScrollLocks ();
The basic equivalents for componentDidMount and componentWillUnmount in React Hooks are:
//componentDidMount
useEffect(() => {
doSomethingOnMount();
}, [])
//componentWillUnmount
useEffect(() => {
return () => {
doSomethingOnUnmount();
}
}, [])
These can also be combined into one useEffect:
useEffect(() => {
doSomethingOnMount();
return () => {
doSomethingOnUnmount();
}
}, [])
This process is called effect clean up, you can read more from the documentation.

add scroll listener by id

Hi i cannot add scroll listener by id, it works with WINDOW but doesnt work in custom scroll element.
Here is my code :
componentDidMount() {
const scrollPanel = document.getElementById('scrollPanel');
scrollPanel.addEventListener('scroll', this.listenToScroll);
}
componentWillUnmount() {
const scrollPanel = document.getElementById('scrollPanel');
scrollPanel.removeEventListener('scroll', this.listenToScroll);
}
listenToScroll = () => {
const { position } = this.state;
const scrollPanel = document.getElementById('scrollPanel');
const winScroll = scrollPanel.scrollTop;
const height =
scrollPanel.scrollHeight -
scrollPanel.clientHeight;
const scrolled = winScroll / height;
console.log('scrolled', scrolled);
this.setState({
position: scrolled,
});
When i try to check some value its never changes
There must be an event handler inside the element which creates the scrolling element:
Here in your case, change the componentDidMount() to below:
componentDidMount() {
document.addEventListener(
"scroll",
this.listenToScroll,
true // Capture event
);
}
listenToScroll = () => {
//Your custom code
alert("click triggered");
};
I tried and it works
You might want to check this answer where I got the reference from :
https://stackoverflow.com/a/30475606

How to detect if screen size has changed to mobile in React?

I am developing a web app with React and need to detect when the screen size has entered the mobile break-point in order to change the state.
Specifically I need my sidenav to be collapsed when the user enters mobile mode and that is controlled with a boolean stored in the state within the component.
What I did is adding an event listener after component mount:
componentDidMount() {
window.addEventListener("resize", this.resize.bind(this));
this.resize();
}
resize() {
this.setState({hideNav: window.innerWidth <= 760});
}
componentWillUnmount() {
window.removeEventListener("resize", this.resize.bind(this));
}
EDIT:
To save state updates, I changed the "resize" a bit, just to be updated only when there is a change in the window width.
resize() {
let currentHideNav = (window.innerWidth <= 760);
if (currentHideNav !== this.state.hideNav) {
this.setState({hideNav: currentHideNav});
}
}
UPDATE: Time to use hooks!
If you're component is functional, and you use hooks - then you can use the useMediaQuery hook, from react-responsive package.
import { useMediaQuery } from 'react-responsive';
...
const isMobile = useMediaQuery({ query: `(max-width: 760px)` });
After using this hook, "isMobile" will be update upon screen resize, and will re-render the component. Much nicer!
const [isMobile, setIsMobile] = useState(false)
//choose the screen size
const handleResize = () => {
if (window.innerWidth < 720) {
setIsMobile(true)
} else {
setIsMobile(false)
}
}
// create an event listener
useEffect(() => {
window.addEventListener("resize", handleResize)
})
// finally you can render components conditionally if isMobile is True or False
Using hooks in React(16.8.0+) refering to:
https://stackoverflow.com/a/36862446/1075499
import { useState, useEffect } from 'react';
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height
};
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
}
This is the same as #Ben Cohen answer but after attaching your function to eventListner, also remove it on componentWillUnmount
constructor() {
super();
this.state = { screenWidth: null };
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
componentDidMount() {
window.addEventListener("resize", this.updateWindowDimensions());
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateWindowDimensions)
}
updateWindowDimensions() {
this.setState({ screenWidth: window.innerWidth });
}
hey I just published a npm package for this issue.
Check it out https://www.npmjs.com/package/react-getscreen
import React, { Component } from 'react';
import {withGetScreen} from 'react-getscreen'
class Test extends Component {
render() {
if (this.props.isMobile()) return <div>Mobile</div>;
if (this.props.isTablet()) return <div>Tablet</div>;
return <div>Desktop</div>;
}
}
export default withGetScreen(Test);
//or you may set your own breakpoints by providing an options object
const options = {mobileLimit: 500, tabletLimit: 800}
export default withGetScreen(Test, options);
There are multiple ways to archive this first way is with CSS using this class
#media screen and (max-width: 576px) {}
any class inside this tag will only be visible when the screen is equal or less than 576px
the second way is to use the event listener
something like this
constructor(props)
{
super(props);
this.state = {
isToggle: null
}
this.resizeScreen = this.resizeScreen.bind(this);
}
componentDidMount() {
window.addEventListener("resize", this.resizeScreen());
}
resizeScreen() {
if(window.innerWidth === 576)
{
this.setState({isToggle:'I was resized'});
}
}
even with the event listener I still prefer the CSS way since we can use multiple screen sizes without further js coding.
I hope this helps!
The react-screentype-hook library allows you to do this out of the box.
https://www.npmjs.com/package/react-screentype-hook
You could use the default breakpoints it provides as follows
const screenType = useScreenType();
screenType has the following shape
{
isLargeDesktop: Boolean,
isDesktop: Boolean,
isMobile: Boolean,
isTablet: Boolean
}
Or you could even configure your custom breakpoints like this
const screenType = useScreenType({
mobile: 400,
tablet: 800,
desktop: 1000,
largeDesktop: 1600
});
For Next.js Here is a custom hook
import { useState, useEffect } from 'react';
export default function useScreenWidth() {
const [windowWidth, setWindowWidth] = useState(null);
const isWindow = typeof window !== 'undefined';
const getWidth = () => isWindow ? window.innerWidth : windowWidth;
const resize = () => setWindowWidth(getWidth());
useEffect(() => {
if (isWindow) {
setWindowWidth(getWidth());
window.addEventListener('resize', resize);
return () => window.removeEventListener('resize', resize);
}
//eslint-disable-next-line
}, [isWindow]);
return windowWidth;
}
In a component, it returns the width size of the viewport, which can then be compared with a given numeric value
const widthSize = useScreenWidth()
const mobileWidth = 400
if(widthSize > mobileWidth){
//logic for desktop
}
if(widthSize <= mobileWidth){
//logic for mobile
}
In Functional Component, we can detect screen size by useTheme and useMediaQuery.
const theme = useTheme();
const xs = useMediaQuery(theme.breakpoints.only('xs'));
const sm = useMediaQuery(theme.breakpoints.only('sm'));
const md = useMediaQuery(theme.breakpoints.only('md'));
const lg = useMediaQuery(theme.breakpoints.only('lg'));
const xl = useMediaQuery(theme.breakpoints.only('xl'));

Resources