Reputation: 106
I have a counter on a certain part of the page which has to be triggered only once when the scroll reaches that element. I can see various solutions in jQuery but I'm looking for the answer in terms of react or pure javascript.
I implemented it using states and react hooks but I see some instability all the time triggering more than once even if I preserve the state and use conditional statements. I'm not sure where I was going wrong.
Here is the react class component(I switched to class from function due to continuous warning while using useEffect)
Update: I found a working solution as below. If there's any other better method to do this please let me know.
import React, { useState, useCallback, useEffect } from "react";
export default function DataCounter({ count, title }) {
const [number, setNumber] = useState(0);
const [trigger, setTrigger] = useState(false);
const increment = useCallback(() => {
const counter = (length = 2000) => {
setNumber(0);
let n = count;
let start = Date.now();
let end = start + length;
let interval = length / n;
const sInt = setInterval(() => {
let time = Date.now();
if (time < end) {
let count = Math.floor((time - start) / interval);
setNumber(count);
} else {
setNumber(n);
clearInterval(sInt);
}
}, interval);
};
counter();
}, [count]);
document.addEventListener("scroll", async () => {
const element = await document.getElementById("data");
const elementPosition = await element.getBoundingClientRect().top;
if (window.pageYOffset > elementPosition) setTrigger(true);
});
useEffect(() => {
if (trigger) {
increment();
}
}, [trigger, increment]);
return (
<div className="counter">
<div className="counter-number">{number}</div>
<p className="counter-title">{title}</p>
</div>
);
}
Initial version:
import React from "react";
export default class Counter extends React.Component {
state = {
number: 0,
loaded: false
};
increment(n, length = 2000){
this.setState({ number: 0 });
let start = Date.now();
let end = start + length;
let interval = length / n;
const sInt = setInterval(() => {
let time = Date.now();
if (time < end) {
let count = Math.floor((time - start) / interval);
this.setState({ number: count });
} else {
this.setState({ number: n });
clearInterval(sInt);
}
}, interval);
};
async handleScroll() {
const element = await document.getElementById("data");
const elementPosition = await element.getBoundingClientRect().bottom;
if (window.pageYOffset > elementPosition) {
if(!this.state.loaded) {
this.increment(100);
this.setState({ loaded: true });
}
return;
}
}
componentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
render() {
return (
<div className="counter">
<div className="counter-number">{this.state.number}</div>
<p className="counter-title">{this.props.title}</p>
</div>
);
}
}
Upvotes: 1
Views: 816
Reputation: 443
I found a problem with your code
You are registering 'scroll' event listener inside the body of the function component. This is why the scroll event is triggered multiple times.
Because every rendering of the function component will add one additional scroll event to the DOM
The correct way to register and cleanup any DOM event is inside the 'useEffect' react hook like this
const handleScrollEvent = async () => {
const element = await document.getElementById("data");
const elementPosition = await element.getBoundingClientRect().top;
if (window.pageYOffset > elementPosition) setTrigger(true);
}
const registerEvent = () => {
document.addEventListener("scroll", handleScrollEvent);
}
const unRegisterEvent = () => {
document.removeEventListener("scroll", handleScrollEvent);
}
useEffect(() => {
if (trigger) {
increment();
}
//Register the event inside useEffect after the component mounts
registerEvent();
//Cleanup the registered event once the component is unmounted from
//the DOM
return unRegisterEvent;
}, [trigger, increment]);
Upvotes: 1
Reputation: 878
Firstly, you declared your class method in a wrong way. Since you are using the ES6 way to declare the state (you are not using a constructor), so you have to declare your class method in ES6 way as well. Such as,
increment = (n, length = 2000) => {
// so you can access *this* variable in this scope
...
}
And this goes the same for handleScroll
method. If you want to declare a helper method like what you did in your code:
increment(n, length = 2000){
...
}
Then you need to declare a constructor and bind this
in the constructor. You can find the reference here.
Secondly, it is absolutely unnecessary to have handleScroll
as an async function. There is no promise being returned in this method.
Last but not the least, always remember to remove the scroll listener in componentWillUnmount
, so you won't have a stack of listeners listening to windows scroll event when the user switches among routes or the component gets mounted again.
Here is a working example on codesandbox.
Upvotes: 1