useEffect used for countdown timer, re-renders whole page each second - javascript

In a React app, I created countdown timer:
const [showSec, setShowSec] = useState(15);
useEffect(() => {
const timer =
showSec > 0 && setInterval(() => setShowSec(showSec - 1), 1000);
if (showSec === 0) {
setShowEnterCode(false);
}
return () => clearInterval(timer);
}, [showSec]);
I show it like that:
<span>{showSec}</span>
and in another place in my code I have something like this for showing some errors:
{props.args.errorFormat === 2 ? (
<ErrorSnackBar args={ErrorSnackbarArgs} />
) : (
<></>
)}
But my problem is that, every time that countdown counts, this part of code executes again. It seems re rendering happens. for example when that condition is true, It should show an error with Material-UI's SnackBar but because of that countdown timer, it restart showing the error constantly. I cant prevent this issue. what I have to do?

You can try the following code, it has Timer as pure component and you can pass in setShowEnterCode and optionally seconds. Make sure that setShowEnterCode in App does not change when App re renders:
const { useEffect, useState } = React;
//make this a pure component so it won't re render when props
// don't change and parent re renders. It re renders when
// local state changes
const Timer = React.memo(function Timer({
setShowEnterCode,
seconds = 15,
}) {
const [showSec, setShowSec] = useState(seconds);
//you can pass in seconds (default is 15) and it'll
// set the initial duration
useEffect(() => setShowSec(seconds), [seconds]);
useEffect(() => {
const timer =
showSec > 0 &&
//effect runs every time showSec changes so
// no need for the interval it only runs once
setTimeout(() => setShowSec(showSec - 1), 1000);
if (showSec === 0) {
setShowEnterCode(false);
}
return () => clearInterval(timer);
//make sure setShowEnterCode does not change
// when App re renders
}, [setShowEnterCode, showSec]);
return <span>{showSec}</span>;
});
function App() {
const [count, setCount] = React.useState(1);
const [showEnterCode, setShowEnterCode] = React.useState(
true
);
//this effect causes App to re render 10 times a second
React.useEffect(() => {
const i = setInterval(() => {
setCount((c) => c + 1);
}, 100);
return () => clearInterval(i);
});
return (
<div>
<div>app counter:{count}</div>
{showEnterCode && (
<Timer setShowEnterCode={setShowEnterCode} />
)}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

React Timer using functional Hooks | Timer is not stopping

I want to create a time of 60s on react using hooks useState and useEffects This is what i am doing
import '../assets/css/timer.css'
import { useState, useEffect } from 'react'
const Timer = () =>{
const [ time, setTime ] = useState(0);
useEffect(()=>{
if(time!==60){
setInterval(()=>{
setTime(prevTime => prevTime+1) ;
}, 1000);
}
}, [])
return(
<>
<div className="circular">
<div className="inner"></div>
<div className="outer"></div>
<div className="numb">
{time} // Place where i am displaying time
</div>
<div className="circle">
<div className="dot">
<span></span>
</div>
<div className="bar left">
<div className="progress"></div>
</div>
<div className="bar right">
<div className="progress"></div>
</div>
</div>
</div>
</>
)
}
export default Timer
Timer is not stopping. It continues to go on for ever. I have tried this too
useEffect(()=>{
setInterval(()=>{
if(time!==60)
setTime(prevTime => prevTime+1) ;
}, 1000);
}, [])
Can some please explain where things are going wrong.
useEffect(..., []) will only run once, so time inside of it will never update. So you need to check prevTime inside of the setTime function, and then only increment if it's not 60. If it is, you should clear the interval, and then you should clear the interval in the cleanup of useEffect:
useEffect(()=>{
const i = setInterval(() => {
setTime(prevTime => {
if (prevTime !== 60) return prevTime+1;
clearInterval(i);
return prevTime;
});
}, 1000);
return () => clearInterval(i);
}, [])
You are close to having it working with your first attempt, but there are a few problems.
The main problem is that you pass an empty dependency array, meaning it will only run on the first render and not be updated on successive renders. Secondly you don't provide a return or 'clean up' meaning the interval is never cleared.
useEffect(() => {
if (time < 60) {
const timer = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => clearInterval(timer);
}
}, [time])
Here we pass time in the dependency array and conditionally set the interval if time is less than your end time, 60 in your case but I shortened it to 5 so that you can see it stop. We also pass a return callback that will clear the interval at the end of each render cycle.
With this set up every time the setInterval updates the time state the useEffect will clear the interval at the end of the previous render, and then re-run in the current render setting the interval again if time is less than the limit.
The advantage of using the React render cycle this way, instead of clearing the interval in the interval callback, is that it gives you granular control of your timeout – allowing you to easily add further checks or extend/shorten the time based on other state values.
const { useState, useEffect } = React;
const Timer = () => {
const [time, setTime] = useState(0);
useEffect(() => {
if (time < 5) {
const timer = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => clearInterval(timer);
}
}, [time])
return (
<div className="circular">
<div className="numb">
{time}
</div>
</div>
)
}
ReactDOM.render(
<Timer />,
document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You have to clear setTimeout in Unmount state
for functional component
// Funtional component
useEffect(()=>{
const i = setInterval(() => {
setTime(prevTime => {
if (prevTime !== 60) return prevTime+1;
clearInterval(i);
return prevTime;
});
}, 1000);
return () => clearInterval(i);
}, [])
For class component
// Class component
componentWillUnmount() {
this.clearInterval()
}

setTimout with pause/resume counter not updating on render

I would like to setup a counter which can be paused as well as resumed in React.js. But whatever I have tried so far is working the functionality part (pause/resume is working) but it's not updating the counter on render. Below is my code:
const ProgressBar = (props) => {
const [isPlay, setisPlay] = useState(false);
const [progressText, setProgressText] = useState(
props.duration ? props.duration : 20
);
var elapsed,
secondsLeft = 20;
const timer = () => {
// setInterval for every second
var countdown = setInterval(() => {
// if allowed time is used up, clear interval
if (secondsLeft < 0) {
clearInterval(countdown);
return;
}
// if paused, record elapsed time and return
if (isPlay === true) {
elapsed = secondsLeft;
return;
}
// decrement seconds left
secondsLeft--;
console.warn(secondsLeft);
}, 1000);
};
timer();
const stopProgress = () => {
setisPlay(!isPlay);
if (isPlay === false) {
secondsLeft = elapsed;
}
};
return (
<>
<p>{secondsLeft}</p>
</>
);
};
export default ProgressBar;
I have tried React.js state, global var type, global let type, react ref so far to make the variable global but none of them worked..
So basically why does your example not work?
Your secondsLeft variable not connected to your JSX. So each time your component rerendered it creates a new secondsLeft variable with a value of 20 (Because rerendering is simply the execution of your function that returns JSX)
How to make your variable values persist - useState or useReducer hook for react functional component or state for class based one. So react will store all the values for you for the next rerender cycle.
Second issue is React doesn't rerender your component, it just doesn't know when it should. So what causes rerendering of your component -
Props change
State change
Context change
adding/removing your component from the DOM
Maybe I missing some other cases
So example below works fine for me
import { useEffect, useState } from "react";
function App() {
const [pause, setPause] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(20);
const timer = () => {
var countdown = setInterval(() => {
if (secondsLeft <= 0) {
clearInterval(countdown);
return;
}
if (pause === true) {
clearInterval(countdown);
return;
}
setSecondsLeft((sec) => sec - 1);
}, 1000);
return () => {
clearInterval(countdown);
};
};
useEffect(timer, [secondsLeft, pause]);
const pauseTimer = () => {
setPause((pause) => !pause);
};
return (
<div>
<span>Seconds Left</span>
<p>{secondsLeft}</p>
<button onClick={pauseTimer}>{pause ? "Start" : "Pause"}</button>
</div>
);
}
import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
var timer = null;
function App() {
const [counter, setCounter] = useState(0);
const [isplayin, setIsPlaying] = useState(false);
const pause = () => {
setIsPlaying(false);
clearInterval(timer);
};
const reset = () => {
setIsPlaying(false);
setCounter(0);
clearInterval(timer);
};
const play = () => {
setIsPlaying(true);
timer = setInterval(() => {
setCounter((prev) => prev + 1);
}, 1000);
};
return (
<div className="App">
<p>Counter</p>
<h1>{counter}</h1>
{isplayin ? (
<>
<button onClick={() => pause()}>Pause</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<>
{counter > 0 ? (
<>
<button onClick={() => play()}>Resume</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<button onClick={() => play()}>Start</button>
)}
</>
)}
</div>
);
}
export default App;

React interval using old state inside of useEffect

I ran into a situation where I set an interval timer from inside useEffect. I can access component variables and state inside the useEffect, and the interval timer runs as expected. However, the timer callback doesn't have access to the component variables / state. Normally, I would expect this to be an issue with "this". However, I do not believe "this" is the the case here. No puns were intended. I have included a simple example below:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
The console outputs only count=0 each second. I know that there's a way to pass a function to the setCount which updates current state and that works in this trivial example. However, that was not the point I was trying to make. The real code is much more complex than what I showed here. My real code looks at current state objects that are being managed by async thunk actions. Also, I am aware that I didn't include the cleanup function for when the component dismounts. I didn't need that for this simple example.
The first time you run the useEffect the intervalSet variable is set to true and your interval function is created using the current value (0).
On subsequent runs of the useEffect it does not recreate the interval due to the intervalSet check and continues to run the existing interval where count is the original value (0).
You are making this more complicated than it needs to be.
The useState set function can take a function which is passed the current value of the state and returns the new value, i.e. setCount(currentValue => newValue);
An interval should always be cleared when the component is unmounted otherwise you will get issues when it attempts to set the state and the state no longer exists.
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
You can run the code and see this working below.
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you need a more complex implementation as mention in your comment on another answer, you should try using a ref perhaps. For example, this is a custom interval hook I use in my projects. You can see there is an effect that updates callback if it changes.
This ensures you always have the most recent state values and you don't need to use the custom updater function syntax like setCount(count => count + 1).
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
This is a very flexible option you could use. However, this hook assumes you want to start your interval when the component mounts. Your code example leads me to believe you want this to start based on the state change of the intervalSet boolean. You could update the custom interval hook, or implement this in your component.
It would look like this in your example:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The problem is the interval is created only once and keeps pointing to the same state value. What I would suggest - move firing the interval to separate useEffect, so it starts when the component mounts. Store interval in a variable so you are able to restart it or clear. Lastly - clear it with every unmount.
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

State within useEffect not updating

I'm currently building a timer in ReactJS for practice. Currently I have two components, a Timer component which displays the time as well as sets an interval upon mounting. I also have an App component which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.
My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:
if(!paused)
tick(time => time+1);
which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
}, []);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
So, my question is:
Why isn't my current code working (why is paused not updating within useEffect)?
Is there any way to make the above code work so that I can "pause" my interval which is set within the useEffect()?
To stop the timer return the function clearing the interval from useEffect().
React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
(source: Using the Effect Hook - Effects with Cleanup)
You should also pass the paused in dependencies array to stop useEffect creating new intervals on each re-render. If you add [paused] it'll only create new interval when paused change.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
return () => clearInterval(timer);
}, [paused]);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The setInterval captures the paused value the first time, you'll need to remove the interval, recreate it everytime paused is changed.
You can check this article for more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks
You can use Dan's useInterval hook. I can't explain it better than him why you should use this.
The hook looks like this.
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Then use it in your Timer component like this.
const Timer = ({ time, paused, tick }) => {
useInterval(() => {
if (!paused) tick(time => time + 1);
}, 1000);
return <p>{time}s</p>;
};
I believe the difference is that useInterval hook is aware of its dependencies (paused) while setInterval isn't.

How make react countdown timer

i'm trying to do countdown timer with react. It will be basically countdown from 10 to 0 and when 0 i will call some function.
i found ideally for me some example: https://codesandbox.io/s/0q453m77nw?from-embed
but it's a class component i wan't to do that with functional component and hooks but i can't.
i tried:
function App() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
setSeconds(setInterval(seconds, 1000));
}, []);
useEffect(() => {
tick();
});
function tick() {
if (seconds > 0) {
setSeconds(seconds - 1)
} else {
clearInterval(seconds);
}
}
return (
<div className="App">
<div
{seconds}
</div>
</div>
);
}
export default App;
it's count down from 10 to 0 very quickly not in 10 seconds.
where i mistake ?
It appears the multiple useEffect hooks are causing the countdown to run more than once per second.
Here's a simplified solution, where we check the seconds in the useEffect hook and either:
Use setTimeout to update seconds after 1 second, or
Do something else (the function you want to call at the end of the countdown)
There are some downsides to this method, see below.
function App() {
const [seconds, setSeconds] = React.useState(10);
React.useEffect(() => {
if (seconds > 0) {
setTimeout(() => setSeconds(seconds - 1), 1000);
} else {
setSeconds('BOOOOM!');
}
});
return (
<div className="App">
<div>
{seconds}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Downsides
Using setInterval has the downside that it could be stopped - for example, the component is unmounted, you navigate to a different tab, or close your computer. If the timer requires more robustness, the better alternative would be to store an endTime in the state (like a global store or context) and have your component check the current time against the endTime to calculate the countdown.
Do you care about precision? If so, you don't want setInterval. If you don't care about precision (and you probably don't) then you can schedule a call to tick() on an interval, not the other way around.
const TimeoutComponent extends Component {
constructor(props) {
super(props);
this.state = { countdown: 10 };
this.timer = setInterval(() => this.tick(), props.timeout || 10000);
}
tick() {
const current = this.state.countdown;
if (current === 0) {
this.transition();
} else {
this.setState({ countdown: current - 1 });
}
}
transition() {
clearInterval(this.timer);
// do something else here, presumably.
}
render() {
return <div className="timer">{this.state.countDown}</div>;
}
}
This depends on your logic a little bit. In the current situation your useEffect where you run your tick method is running on every render. You can find a naive example below.
function App() {
const [seconds, setSeconds] = useState(10);
const [done, setDone] = useState(false);
const foo = useRef();
useEffect(() => {
function tick() {
setSeconds(prevSeconds => prevSeconds - 1)
}
foo.current = setInterval(() => tick(), 1000)
}, []);
useEffect(() => {
if (seconds === 0) {
clearInterval(foo.current);
setDone(true);
}
}, [seconds])
return (
<div className="App">
{seconds}
{done && <p>Count down is done.</p>}
</div>
);
}
In the first effect we are doing the countdown. Using callback one for setting state since interval creates a closure. In the second effect we are checking our condition.
Simply use this snippet, As it will also help to memoize the timeout callback.
const [timer, setTimer] = useState(60);
const timeOutCallback = useCallback(() => setTimer(currTimer => currTimer - 1), []);
useEffect(() => {
timer > 0 && setTimeout(timeOutCallback, 1000);
}, [timer, timeOutCallback]);
console.log(timer);
Hope this will help you or somebody else.
Happy Coding!

Categories

Resources