React countdown timer not accurate - javascript

Beginner here. I'm trying to make a countdown timer from 3 to 0. The app renders the seconds to the screen, but it does so really quickly. I tried changing the interval but it doesn't seem to make the time anymore accurate. I don't know what is going wrong. Any help is appreciated.
import React from "react";
export default class Timer extends React.Component {
constructor(){
super();
this.state = {
time: 3,
}
this.countdown = this.countdown.bind(this);
this.timer = this.timer.bind(this)
}
timer(){
let interval = setInterval(() => this.countdown(interval),1000)
return this.state.time
}
countdown(t){
if(this.state.time == null)
{
console.log("NULL")
}
let myTime = this.state.time
if(myTime > 0) {
myTime--;
this.setState({time: myTime})
console.log(myTime)
} else {
clearInterval(t)
}
return myTime;
}
render() {
return (
<div id = "Timer">
<p>
{this.timer()}
</p>
</div>
);
}
}

The user that firs commented your post is right. But let me clarify.
This is what I think that is happening. The first time your component renders execute the timer() method, which set the timer interval. After the first second, the interval callback is executed, which change the component state, and react schedule a re-render of your component. Then, the component re-renders itself, and execute the timer() function again before the 2nd second (please forgive this redundant phrase) which sets a new interval. And this occurs until you clear the interval, which is the last interval your code have set. That is why you notice the value of variable time change oddly fast.
You should do something like this: (this is your very same code with a few changes, may be is more useful for you to understand. Then you can give your own style or personal flavor)
import React from "react";
export default class Timer extends React.Component {
constructor(){
super();
this.state = {
time: 3,
}
this.countdown = this.countdown.bind(this);
this.timer = this.timer.bind(this)
}
componentDidMount() {
this.interval = setInterval(() =>
this.countdown(interval),1000
);
}
componentWillUnmount() {
if (this.interval) {
clearInterval(this.interval);
}
}
countdown(){
if(this.state.time == null)
{
console.log("NULL")
}
let myTime = this.state.time
if(myTime > 0) {
myTime--;
this.setState({time: myTime})
console.log(myTime)
} else {
clearInterval(this.interval)
}
return myTime;
}
render() {
return (
<div id = "Timer">
<p>
{this.state.time}
</p>
</div>
);
}
}
Cheers!

I would use componentDidMount here to start the interval going. You only want to create the interval once and then clean it up when either it finishes counting down or if the component unmounts before the timer has reached 0. You can build extra functionality ontop of this to do things like stop / start again... etc.
export default class Timer extends React.Component {
state = {
time: this.props.start || 3
};
options = {
interval: this.props.interval || 1000
step: this.props.step || 1
};
interval = null;
componentDidMount() {
this.countdown()
}
componentWillUnmount() {
clearInterval(this.interval)
}
tick = () => {
this.setState(
({ time }) => ({ time: time - this.options.step }),
() => {
if (this.state.time === 0) {
clearInterval(this.interval);
}
}
);
}
countdown = () => {
this.interval = setInterval(this.tick, this.options.interval);
}
render() {
return (
<div id="Timer">
<p>{this.state.time}</p>
</div>
);
}
}
Here's a demo to play with :)

Related

why I can't stop interval in react functional component

In my code I have some problems with intervals. I have situation like this
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleClick() {
const timer = setInterval(() => {
if (count >= 10) {
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
export default App;
what am trying to do is to start an interval when user click a button and stop it when counter reaches 10, but it never stops and debugger says that inside setInterval count is always 0. Does anybody know what is the problem? I also found that if I rewrite component to class component like this
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick() {
let myInterval = setInterval(() => {
if (this.state.count >= 10) {
clearInterval(myInterval);
} else {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
}, 1000);
}
render() {
return (
<>
<button onClick={this.handleClick.bind(this)}>start</button>
<p>{this.state.count}</p>
</>
);
}
}
it works perfectly.
I don't know what's happening and I spend almost day trying to figure out.
So does anybody know why class component works and why functional component doesn't?
Thanks in advance
You're seeing a stale count in the setInterval callback function since it captures the value at the time you're starting the timer.
You can use the "ref boxing" pattern to have a readable reference to the latest value of the state atom:
import {useState, useRef} from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(null);
countRef.current = count; // Update the boxed value on render
function handleClick() {
const timer = setInterval(() => {
if (countRef.current >= 10) { // Read the boxed value
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
Note that you're currently not correctly cleaning up the timer on unmount, leading to React warning you about that when you'd unmount your app. The same holds for your class component.

React Native setInterval function for multiple function with different duration

I have two functions in React Native Component, in that one should refresh every 10s and another one should refresh every 1s. I have implemented setInterval() function for refreshing on componentDidMount() and clearInterval() on componentWillUnmount(), but am facing trouble it takes only one function which has the lowest duration. But am achieving result if set duration of both function same duration.
Here is the example
...
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevel: 0,
btState: null,
};
}
componentDidMount() {
this.getBtLevels();
this.getBtState();
this.batLS2();
this.batLS10();
}
componentWillUnmount() {
clearInterval(() => { this.batLSS(); this.batLS10(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({btLevel: res.level}),
);
};
getBtLevelArcs = () => {
fetch(apiUrl).then((res) =>
this.setState({btLevelArc: res.level}),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({BtState: res.state}),
);
};
batLS10 = () => {
setInterval(() => {
this.getBtLevelArcs();
}, 10000);
};
batLS2 = () => {
setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
};
...
On the above Code this.getBtLevels(); this.getBtState(); fetch value every 1 seconds and this.getBtLevelArcs(); fetch value every 10 secounds. In this this.getBtLevels(); this.getBtLevelArcs(); functions get same value. But one should refresh every 1 second and another one every 10 seconds. Here am getting is 1s setInterval function this.batLS2() is refresh whole component.
How can I achieve this one should refresh value 1s and another 10s.
here is the Original Version code. Expo
Issue
clearInterval works by being passed the reference returned from setInterval, i.e. this.timerId = setInterval(... and clearInterval(this.timerId).
What I suspect is occurring is you edited you code and ran, which set an interval callback (but didn't clear it), and then edited and re-ran your code, which sets another interval callback (again, not cleared), etc... You basically aren't cleaning up the interval callbacks when the component unmounts (like a page refresh).
Solution
Add a timer variable for each interval timer
constructor(props) {
super(props);
...
this.timer1 = null;
this.timer2 = null;
}
Clear each interval on dismount
componentWillUnmount() {
clearInterval(this.timer1)
clearInterval(this.timer2)
}
Save the timer ref
batLS10 = () => {
this.timer2 = setInterval(() => {
this.getBtLevelArcs();
}, 10000);
};
batLS2 = () => {
this.timer1 = setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
};
What I understood by the example and statement is you want getBtLevels and getBtState to be called after every 1 sec and getBtLevelArcs after every 10 seconds.
But what happens is when getBtState and getBtLevels invoke, their setState updates the whole component, which is not acceptable in your case.
Ideally this should not be a problem, because all the three functions have different states. btLevel, btLevelArc and btState. Updating one state should not impact the other one. But that totally depends upon your UI logic.
If that is still a problem: what you can do. You can split your component into two components. First one will hold the UI related to getBtLevels and getBtState and second component will contain UI related to getBtLevelArcs. This is required because setState will re-render the whole component.
Code will be something like this:
class App extends React.Component {
...
//some common handlers for SubApp1 and SubApp2
...
render() {
return (
<React.Fragment>
<SubApp1 />
<SubApp2 />
</React.Fragment>
)
}
class SubApp1 extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevel: 0,
btState: null,
};
}
componentDidMount() {
this.getBtLevels();
this.getBtState();
this.batLS2();
}
componentWillUnmount() {
clearInterval(() => { this.batLSS(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevel: res.level }),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({ BtState: res.state }),
);
};
batLS2 = () => {
setInterval(() => {
this.getBtLevels();
this.getBtState();
}, 1000);
}
...
...
class SubApp2 extends React.Component {
constructor(props) {
super(props);
this.state = {
btLevelArc: 'some default value'
};
}
componentDidMount() {
this.batLS10();
}
componentWillUnmount() {
clearInterval(() => { this.batLS10(); });
}
getBtLevels = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevel: res.level }),
);
};
getBtState = () => {
fetch(apiUrl).then((res) =>
this.setState({ BtState: res.state }),
);
};
getBtLevelArcs = () => {
fetch(apiUrl).then((res) =>
this.setState({ btLevelArc: res.level }),
);
};
...
...

How to correctly work with data from React Context in useEffect/useCallback-hook

I'm using a React Context to store data and to provide functionality to modify these data.
Now, I'm trying to convert a Class Component into a Functional Component using React Hooks.
While everything is working as expected in the Class, I don't get it to work in the Functional Component.
Since my applications code is a bit more complex, I've created this small example (JSFiddle link), which allows to reproduce the problem:
First the Context, which is the same for both, the Class and the Functional Component:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
constructor (props) {
super(props);
this.increase = this.increase.bind(this);
this.reset = this.reset.bind(this);
this.state = {
current: 0,
increase: this.increase,
reset: this.reset
}
}
render () {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase (step) {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset () {
this.setState({
current: 0
});
}
}
Now, here is the Class component, which works just fine:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.increaseByOne = this.increaseByOne.bind(this);
}
componentDidMount () {
setInterval(this.increaseByOne, 1000);
}
render () {
const count = this.context;
return (
<div>{count.current}</div>
);
}
increaseByOne () {
const count = this.context;
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}
}
MyComponent.contextType = MyContext;
The expected result is, that it counts to 5, in an interval of one second - and then starts again from 0.
And here is the converted Functional Component:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, []);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Instead of resetting the counter at 5, it resumes counting.
The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.
The only way I get this to work, is to adjust the code on the following way:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, [count]);
React.useEffect(() => {
console.log('useEffect');
const interval = setInterval(increaseByOne, 1000);
return () => {
clearInterval(interval);
};
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Now, the increaseByOne callback is recreated on every change of the context, which also means that the effect is called every second.
The result is, that it clears the interval and sets a new one, on every change to the context (You can see that in the browser console).
This may work in this small example, but it changed the original logic, and has a lot more overhead.
My application does not rely on an interval, but it's listening for an event. Removing the event listener and adding it again later, would mean, that I may loose some events, if they are fired between the remove and the binding of the listener, which is done asynchronously by React.
Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?
I've created a fiddle here, to play around with the code above:
https://jsfiddle.net/Jens_Duttke/78y15o9p/
First solution is to put data is changing through time into useRef so it would be accessible by reference not by closure(as well as you access actual this.state in class-based version)
const MyComponent = (props) => {
const countByRef = React.useRef(0);
countByRef.current = React.useContext(MyContext);
React.useEffect(() => {
setInterval(() => {
const count = countByRef.current;
console.log(count.current);
if (count.current === 5) {
count.reset();
} else {
count.increase(1);
}
}, 1000);
}, []);
return (
<div>{countByRef.current.current}</div>
);
}
Another solution is to modify reset and increase to allow functional argument as well as it's possible with setState and useState's updater.
Then it would be
useEffect(() => {
setInterval(() => {
count.increase(current => current === 5? 0: current + 1);
}, 1000);
}, [])
PS also hope you have not missed clean up function in your real code:
useEffect(() => {
const timerId = setInterval(..., 1000);
return () => {clearInterval(timerId);};
}, [])
otherwise you will have memory leakage
If the increaseByOne function doesn't need to know the actual count.current, you can avoid recreating it. In the context create a new function called is that checks if the current is equal a value:
is = n => this.state.current === n;
And use this function in the increaseByOne function:
if (count.is(5)) {
count.reset();
}
Example:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase = (step) => {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset = () => {
this.setState({
current: 0
});
}
is = n => this.state.current === n;
state = {
current: 0,
increase: this.increase,
reset: this.reset,
is: this.is
};
}
const MyComponent = (props) => {
const { increase, reset, is, current } = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
if (is(5)) {
reset();
} else {
increase(1);
}
}, [increase, reset, is]);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{current}</div>
);
}
const App = () => (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
ReactDOM.render( <
App / > ,
document.querySelector("#app")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>

Stop interval function when changing site

I am writing a website using react . In one component I have a setInterval() function which gets executed, it updates them DOM. Now, when I change onto another site with my router (react-router-dom) the setInterval() function crashed, because it cannot find the DOM elements to update. How do I go on about this? I though I use componentWillUnmount() but the same error occurs.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
stop: false,
}
}
componentWillUnmount() {
if(!this.state.stop) {
this.setState({
stop: true,
})
}
}
_stop = (counter) => {
clearInterval(counter);
}
_someFunc = () => {
...
}
render() {
...
const update = setInterval(function () {
document.getElementById('some-id').innerText = this._someFunc();
}, 1000);
if(this.state.stop) {
this._stop(update)
}
return (
<p id='some-id'></p>
)
}
}
export default Counter;
TypeError: document.getElementById(...) is null.
How do I stop the interval?
Changes:
1- Put the timer outside of the render method, better to use componentDidMount lifecycle method. By this way timer will be registered only once and after each 1ms it will execute the callback method.
2- Store the timer id in a variable (in instance) to stop it before leaving the page.
Like this:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
stop: false,
}
}
componentDidMount() {
this.timerId = setInterval(function () {
document.getElementById('some-id').innerText = this._someFunc();
}, 1000);
}
componentWillUnmount() {
this._stop();
}
_stop = () => {
clearInterval(this.timerId);
}
_someFunc = () => {
...
}
render() {
return (
<p id='some-id'></p>
)
}
}
export default Counter;
If it is not a performance optimization then do it react way
https://stackoverflow.com/a/39426527/6124657

Clear interval in React class

So, we have this simple React component that receives an integer from the father component. When the button is clicked, we display the integer on the screen and the countdown begins.
The question is how can I stop the countdown. While reading other SO posts, I found about clearInterval(), but it seems I am missing something here.
Any help would be greatly appreciated. Bonus appreciation points will be awarded if someone is kind enough to explain to me why the sample code is not working as expected.
import React from "react";
export default class TestInterval extends React.Component {
constructor(props) {
super(props);
this.state = {
countDown: this.props.countDown, // An integer from father component
}
}
timer = () => {
setInterval(() => {
if (this.state.countDown === 0) {
this.stopCountDown();
}
this.setState( prevState => ({
countDown: prevState.countDown - 1,
}));
}, 1000)
}
startCountDown = () => {
this.timer();
}
stopCountDown = () => {
clearInterval(this.timer); // Not working
}
render () {
return (
<div>
<button onClick={this.startCountDown}>
Start Countdown
</button>
<p>{this.state.countDown}</p>
</div>
);
}
}
You need to store the interval reference returned from setInterval.
From the docs:
It returns an interval ID which uniquely identifies the interval, so
you can remove it later by calling clearInterval().
So your code should look like that for example:
this.interval = setInterval(() => {...
and then clear it:
clearInterval(this.interval);
I would check the condition after the state has truly set (setState is asynchronous) you can do it inside the callback of setState.
this.interval = setInterval(() => {
this.setState(prevState => ({
countDown: prevState.countDown - 1,
}), () => {
if (this.state.countDown === 0) {
this.stopCountDown();
}
});
}, 1000)
Running example:
class TestInterval extends React.Component {
constructor(props) {
super(props);
this.state = {
countDown: this.props.countDown, // An integer from father component
}
}
timer = () => {
this.interval = setInterval(() => {
this.setState(prevState => ({
countDown: prevState.countDown - 1,
}), () => {
if (this.state.countDown === 0) {
this.stopCountDown();
}
});
}, 1000)
}
startCountDown = () => {
this.timer();
}
stopCountDown = () => {
clearInterval(this.interval); // Not working
}
render() {
return (
<div>
<button onClick={this.startCountDown}>
Start Countdown
</button>
<p>{this.state.countDown}</p>
</div>
);
}
}
ReactDOM.render(<TestInterval countDown={3} />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Categories

Resources