React setInterval in useEffect with setTimeout delay - reactjs

I want to run an interval with a delay for the first time it fires. How can I do this with useEffect? Because of the syntax I've found it difficult to achieve what I want to do
The interval function
useEffect(()=>{
const timer = setInterval(() => {
//do something here
return ()=> clearInterval(timer)
}, 1000);
},[/*dependency*/])
The delay function
useEffect(() => {
setTimeout(() => {
//I want to run the interval here, but it will only run once
//because of no dependencies. If i populate the dependencies,
//setTimeout will run more than once.
}, Math.random() * 1000);
}, []);
Sure it is achievable somehow...

getting started
Consider detangling the concerns of your component and writing small pieces. Here we have a useInterval custom hook which strictly defines the setInterval portion of the program. I added some console.log lines so we can observe the effects -
// rough draft
// read on to make sure we get all the parts right
function useInterval (f, delay)
{ const [timer, setTimer] =
useState(null)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
Now when we write MyComp we can handle the setTimeout portion of the program -
function MyComp (props)
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000) // first try
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
}
Now we can useInterval in various parts of our program, and each one can be used differently. All the logic for the start, stop and cleanup is nicely encapsulated in the hook.
Here's a demo you can run to see it working -
const { useState, useEffect } = React
const useInterval = (f, delay) =>
{ const [timer, setTimer] =
useState(undefined)
const start = () =>
{ if (timer) return
console.log("started")
setTimer(setInterval(f, delay))
}
const stop = () =>
{ if (!timer) return
console.log("stopped", timer)
setTimer(clearInterval(timer))
}
useEffect(() => stop, [])
return [start, stop, timer != null]
}
const MyComp = props =>
{ const [counter, setCounter] =
useState(0)
const [start, stop, running] =
useInterval(_ => setCounter(x => x + 1), 1000)
return <div>
{counter}
<button
onClick={start}
disabled={running}
children="Start"
/>
<button
onClick={stop}
disabled={!running}
children="Stop"
/>
</div>
};
ReactDOM.render
( <MyComp/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting it right
We want to make sure our useInterval hook doesn't leave any timed functions running if our timer are stopped or after our components are removed. Let's test them out in a more rigorous example where we can add/remove many timers and start/stop them at any time -
A few fundamental changes were necessary to make to useInterval -
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
Using useInterval in MyTimer component is intuitive. MyTimer is not required to do any sort of cleanup of the interval. Cleanup is automatically handled by useInterval -
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now()) // <-- for demo
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...") // <-- for demo
setTimeout(() => {
console.log("starting...") // <-- for demo
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button onClick={start} disabled={busy} children="Start" />
<button onClick={stop} disabled={!busy} children="Stop" />
</span>
}
The Main component doesn't do anything special. It just manages an array state of MyTimer components. No timer-specific code or clean up is required -
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
Expand the snippet below to see useInterval working in your own browser. Fullscreen mode is recommended for this demo -
const { useState, useEffect } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t = setInterval(f, delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
useState(0)
const [start, stop, busy] =
useInterval(_ => {
console.log("tick", Date.now())
setCounter(x => x + 1)
}, delay)
useEffect(() => {
console.log("delaying...")
setTimeout(() => {
console.log("starting...")
auto && start()
}, 2000)
}, [])
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>
getting advanced
Let's imagine an even more complex useInterval scenario where the timed function, f, and the delay can change -
function useInterval (f, delay = 1000)
{ const [busy, setBusy] = // ...
const interval = useRef(f)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
// ...
const t =
setInterval(_ => interval.current(), delay)
// stop
// ...
}, [busy, delay])
return // ...
}
Now we can edit MyTimer to add the doubler and turbo state -
function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] = useState(0)
const [doubler, setDoubler] = useState(false) // <--
const [turbo, setTurbo] = useState(false) // <--
const [start, stop, busy] =
useInterval
( doubler // <-- doubler changes which f is run
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo // <-- turbo changes delay
? Math.floor(delay / 2)
: delay
)
// ...
Then we add a double and turbo button -
// ...
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
{/* start button ... */}
<button
onClick={toggleDoubler} // <--
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo} // <--
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
{/* stop button ... */}
</span>
}
Expand the snippet below to run the advanced timer demo in your own browser -
const { useState, useEffect, useRef, useCallback } = React
const append = (a = [], x = null) =>
[ ...a, x ]
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
if (pos < 0) return a
return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}
function useInterval (f, delay = 1000)
{ const interval = useRef(f)
const [busy, setBusy] = useState(0)
useEffect(() => {
interval.current = f
}, [f])
useEffect(() => {
// start
if (!busy) return
setBusy(true)
const t =
setInterval(_ => interval.current(), delay)
// stop
return () => {
setBusy(false)
clearInterval(t)
}
}, [busy, delay])
return [
_ => setBusy(true), // start
_ => setBusy(false), // stop
busy // isBusy
]
}
function MyTimer ({ delay = 1000, ... props })
{ const [counter, setCounter] =
useState(0)
const [doubler, setDoubler] = useState(false)
const [turbo, setTurbo] = useState(false)
const [start, stop, busy] =
useInterval
( doubler
? _ => setCounter(x => x * 2)
: _ => setCounter(x => x + 1)
, turbo
? Math.floor(delay / 2)
: delay
)
const toggleTurbo = () =>
setTurbo(t => !t)
const toggleDoubler = () =>
setDoubler(t => !t)
return <span>
{counter}
<button
onClick={start}
disabled={busy}
children="Start"
/>
<button
onClick={toggleDoubler}
disabled={!busy}
children={`Doubler: ${doubler ? "ON" : "OFF"}`}
/>
<button
onClick={toggleTurbo}
disabled={!busy}
children={`Turbo: ${turbo ? "ON" : "OFF"}`}
/>
<button
onClick={stop}
disabled={!busy}
children="Stop"
/>
</span>
}
function Main ()
{ const [timers, setTimers] = useState([])
const addTimer = () =>
setTimers(r => append(r, <MyTimer />))
const destroyTimer = c => () =>
setTimers(r => remove(r, c))
return <main>
<p>Run in expanded mode. Open your developer console</p>
<button
onClick={addTimer}
children="Add Timer"
/>
{ timers.map((c, key) =>
<div key={key}>
{c}
<button
onClick={destroyTimer(c)}
children="Destroy"
/>
</div>
)}
</main>
}
ReactDOM.render
( <Main/>
, document.getElementById("react")
)
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script></script>

I think What you're trying to do is this:
const DelayTimer = props => {
const [value, setvalue] = React.useState("initial");
const [counter, setcounter] = React.useState(0);
React.useEffect(() => {
let timer;
setTimeout(() => {
setvalue("delayed value");
timer = setInterval(() => {
setcounter(c => c + 1);
}, 1000);
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div>
Value:{value} | counter:{counter}
</div>
);
};
// Render it
ReactDOM.render(<DelayTimer />, document.getElementById("react"));
<div id="react"></div>
<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></script>

Is this what you want to achieve? the empty array on useeffect tells it will run this code once the element is rendered
const {useState, useEffect} = React;
// Example stateless functional component
const SFC = props => {
const [value,setvalue] = useState('initial')
const [counter,setcounter] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setvalue('delayed value')
setcounter(counter+1)
clearInterval(timer)
}, 2000);
}, []);
return(<div>
Value:{value} | counter:{counter}
</div>)
};
// Render it
ReactDOM.render(
<SFC/>,
document.getElementById("react")
);
<div id="react"></div>
<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></script>

If you are trying to use a setInterval inside useEffect, I think you switched up the order a bit, it should be like this
const INTERVAL_DELAY = 1000
useEffect(() => {
const interval = setInterval(() => {
/* do repeated stuff */
}, INTERVAL_DELAY)
return () => clearInterval(interval)
})
The interval will start after a delay, so if you want an interval delay of X seconds to start after Y seconds, you have to actually use a delay in setTimeout as Y - X
const INITIAL_DELAY = 10000
const INTERVAL_DELAY = 5000
useEffect(() => {
let interval
setTimeout(() => {
const interval = setInterval(() => {
/* do repeated stuff */
}, INTERVAL_DELAY)
}, INITIAL_DELAY - INTERVAL_DELAY)
return () => clearInterval(interval)
})

Related

React click specific element in setInterval loop

I'm trying to click my element in setInterval loop, so it would be clicked every 10 second, but there's always error click is not a function or cannot read click null
I've tired with useRef and also did nothing.
here is my code:
useEffect(() => {
setInterval(function () {
const handleChangeState = () => {
console.log("Now");
document.getElementById("dice").click();
};
handleChangeState();
}, 10 * 1000);
}, []);
return (
<>
<Dice id="dice" rollingTime="3000" triggers={["click", "P"]} />
</>
);
};
It is often considered anti-pattern in React to query the DOM. You should instead use a React ref to gain access to the underlying DOMNode.
There are a couple ways to use a React ref to invoke a dice roll of the child component. FYI, rollingTime should probably be number type instead of a string if using in any setTimeout calls.
Forward the React ref and attach to the button element and invoke the click handler.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
return (
<>
<h1>Dice</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button ref={ref} id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleChangeState = () => {
console.log("Clicking Dice");
diceRef.current?.click();
};
setInterval(() => {
handleChangeState();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Forward the React ref and invoke the button's callback function directly via the useImperativeHandle hook.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
useImperativeHandle(ref, () => ({
roll
}));
return (
<>
<h1>Dice 2</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleRollDice = () => {
console.log("Roll dice");
diceRef.current.roll();
};
setInterval(() => {
handleRollDice();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Using react-dice-roll
If you examine the react-dice-roll source code you'll see that the Dice component forwards a React ref and uses the useImperativeHandle hook to expose out a rollDice function.
Dice Source
const Dice = forwardRef((props: TProps, ref: React.MutableRefObject<TDiceRef>) => {
...
const handleDiceRoll = (value?: TValue) => {
let diceAudio: HTMLAudioElement;
if (sound) {
diceAudio = new Audio(sound);
diceAudio.play();
}
setRolling(true);
setTimeout(() => {
let rollValue = Math.floor((Math.random() * 6) + 1) as TValue;
if (value) rollValue = value;
if (cheatValue) rollValue = cheatValue;
setRolling(false);
setValue(rollValue);
if (diceAudio) diceAudio.pause();
if (!onRoll) return;
onRoll(rollValue);
}, rollingTime);
};
useImperativeHandle(ref, () => ({ rollDice: handleDiceRoll }));
...
return (
...
)
});
Your code then just needs to create a React ref and pass it to the Dice component, and instantiate the interval in a mounting useEffect hook.
Example:
function App() {
const diceRef = useRef();
useEffect(() => {
const rollDice = () => {
console.log("Rolling Dice");
diceRef.current.rollDice(); // <-- call rollDice function
};
// instantiate interval
setInterval(() => {
rollDice();
}, 10 * 1000);
// immediately invoke so we don't wait 10 seconds for first roll
rollDice();
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}

I want to subtract the value of 'percent' from the function by 0.25 in React

I want to subtract the value of 'percent' from the function by 0.25.
However, subtraction does not work.
I used setState, but I don't know why it doesn't work.
import React, {useState, useRef, useCallback} from 'react';
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback(() =>{
if (intervalRef.current !== null){
return;
}
intervalRef.current = setInterval(()=>{
if (percent > 0){
setPercent(c => c - 0.25);
console.log("percent = ", percent);
}
else {
setPercent(c => 1);
}
}, 1000);
}, []);
return (
<div>
<button onClick={()=>{start()}}>{"Start"}</button>
</div>
);
}
export default Ques;
Issue
The enqueued state updates are working correctly but you've a stale enclosure over the percent state in the interval callback that you are logging, it never will update.
Solution
If you want to log the percent state then use an useEffect hook to log changes.
const Ques = () => {
const [percent, setPercent] = useState(1);
const intervalRef = useRef(null);
useEffect(() => {
console.log("percent = ", percent); // <-- log state changes here
}, [percent]);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setPercent((c) => Math.max(0, c - 0.25)); // <-- simpler updater function
}, 1000);
}, []);
return (
<div>
Percent: {percent * 100}
<button onClick={start}>Start</button>
</div>
);
};
You can create a ref for percent also and chenge its current value as:
codesandbox link
import React, { useRef, useCallback } from "react";
const Ques = () => {
const percentRef = useRef(1);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
console.log("percent = ", percentRef.current);
percentRef.current > 0
? (percentRef.current -= 0.25)
: (percentRef.current = 1);
}, 1000);
}, []);
return (
<div>
<button onClick={start}>Start</button>
</div>
);
};
export default Ques;
I think useCallback and useRef is not a good fit. Below is a minimal verifiable example using useState and useEffect. Note this function appropriately performs cleanup on the timer when the component is unmounted. Click Run to run the code snippet and click start to begin running the effect.
function App() {
const [percent, setPercent] = React.useState(1)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
if (!running) return
const t = window.setTimeout(() => {
setPercent(c => c > 0 ? c - 0.25 : 1)
}, 1000)
return () => window.clearTimeout(t)
}, [running, percent])
return <div>
<button onClick={() => setRunning(true)} children="start" />
<pre>percent: {percent}</pre>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
This may be one possible solution to achieve what is presumed to be the desired objective:
Code Snippet
const {useState, useRef, useCallback} = React;
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback((flag) => {
if (intervalRef.current !== null){
if (flag && flag === 'end') clearInterval(intervalRef.current);
return;
}
intervalRef.current = setInterval(() => {
setPercent(
prev => (prev > 0 ? prev - 0.25 : 1)
);
}, 1000);
}, []);
return (
<div>
percent: {percent} <br/> <br/>
<button onClick={() => start('bgn')}>Start</button>  
<button onClick={() => start('end')}>Stop</button>
</div>
);
}
ReactDOM.render(
<div>
<h3>DEMO</h3>
<Ques />
</div>,
document.getElementById('rd')
);
<div id='rd' />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
Explanation
There are two buttons Start and Stop
Both invoke the same start method, but with different params (flag)
If intervalRef is already set (ie, not null) and flag is end, clear the interval
The percent is added to the UI to see real-time changes to its value
setPercent is modified to use prev (which holds the correct state)

How to reset the timer using UseEffect React Hooks

I'm currently trying to make a pomodoro timer. One feature I noticed on some timers is that if you click reset time, it'll go to the default time (usually 15 minutes).
I want it to be a little more advanced so that when you click reset, it will reset to whatever value you set the timer to.
For example, when you open the screen the default time is 15 minutes. If you add 5 minutes, then press start, then press reset, it should reset to 20 minutes. Instead, my code is resetting it to 15.
Can anyone think of a way to reset time?
Apologies if it's a bad explanation. Thank you in advance.
Here's my code so far using React Hooks:
import './App.css';
function App() {
const [session, setSession] = useState(5)
const [timer, setTimer] = useState(2)
const [isRunning, setIsRunning] = useState(false)
const [resetTime, setResetTime] = useState(900)
let time = new Date(timer * 1000).toISOString().substr(11, 8);
function sessionIncrement() {
setSession(prevSession => session + 1)
}
function sessionDecrement() {
if (session > 0) {
setIsRunning(false)
setSession(prevSession => prevSession > 0 && prevSession - 1)
}
}
function resetTimer() {
setIsRunning(false)
setTimer(resetTime)
}
function increment() {
setIsRunning(false);
setTimer(prevTimer => prevTimer + 300);
}
function decrement() {
setIsRunning(false);
setTimer(prevTimer => prevTimer - 300);
}
useEffect(() => {
if (isRunning) {
const interval = setInterval(() => {
setTimer(prevTimer => prevTimer > 0 && prevTimer - 1)
}, 1000);
if (timer === 0) {
sessionDecrement()
setIsRunning(false)
}
return () => clearInterval(interval)
}
}, [isRunning, session, timer])
useEffect(() => {
setResetTime(timer)
}, [])
return (
<div className="App">
<h1>Session #{session}</h1>
<button onClick={() => sessionDecrement()}>-</button>
<button onClick={() => sessionIncrement()}>+</button>
<h1>{time}</h1>
<button onClick={() => setIsRunning(false)} >Pause</button>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => resetTimer()}>Reset</button>
<button onClick={() => decrement()}>-</button>
<button onClick={() => increment()}>+</button>
</div>
);
}
export default App;
The first thing you'll want to do is change the timer and reset time to use the same value:
const initialTime = 900;
const [session, setSession] = useState(5);
const [timer, setTimer] = useState(initialTime);
const [isRunning, setIsRunning] = useState(false);
const [resetTime, setResetTime] = useState(initialTime);
Then, you'll want to change increment and decrement functions so that they change the reset value as well as the timer value:
function increment() {
setIsRunning(false);
setTimer((prevTimer) => prevTimer + 300);
setResetTime((prevResetTime) => prevResetTime + 300);
}
function decrement() {
setIsRunning(false);
setTimer((prevTimer) => prevTimer - 300);
setResetTime((prevResetTime) => prevResetTime - 300);
}
If I understand you right, you just need to set resetTime to the same value as timer, in the increment and decrement function. Like this:
import './App.css';
function App() {
const [session, setSession] = useState(5)
const [timer, setTimer] = useState(2)
const [isRunning, setIsRunning] = useState(false)
const [resetTime, setResetTime] = useState(900)
let time = new Date(timer * 1000).toISOString().substr(11, 8);
function sessionIncrement() {
setSession(prevSession => session + 1)
}
function sessionDecrement() {
if (session > 0) {
setIsRunning(false)
setSession(prevSession => prevSession > 0 && prevSession - 1)
}
}
function resetTimer() {
setIsRunning(false)
setTimer(resetTime)
}
function increment() {
const newTime = timer + 300
setIsRunning(false);
setTimer(newTime);
setResetTime(newTime)
}
function decrement() {
const newTime = timer - 300
setIsRunning(false);
setTimer(newTime);
setResetTime(newTime)
}
useEffect(() => {
if (isRunning) {
const interval = setInterval(() => {
setTimer(prevTimer => prevTimer > 0 && prevTimer - 1)
}, 1000);
if (timer === 0) {
sessionDecrement()
setIsRunning(false)
}
return () => clearInterval(interval)
}
}, [isRunning, session, timer])
useEffect(() => {
setResetTime(timer)
}, [])
return (
<div className="App">
<h1>Session #{session}</h1>
<button onClick={() => sessionDecrement()}>-</button>
<button onClick={() => sessionIncrement()}>+</button>
<h1>{time}</h1>
<button onClick={() => setIsRunning(false)} >Pause</button>
<button onClick={() => setIsRunning(true)}>Start</button>
<button onClick={() => resetTimer()}>Reset</button>
<button onClick={() => decrement()}>-</button>
<button onClick={() => increment()}>+</button>
</div>
);
}
export default App;

How to stop Timer when button is clicked in React?

How can I stop the both the timer when my button is clicked in reactjs.
I have noticed that when my timer is running my whole component is re-rendering every-time how to avoid this part.
export default function App() {
const [counterSecond, setCounterSecond] = React.useState(0);
const [counter, setCounter] = React.useState(120);
const [time, setTime] = React.useState("");
React.useEffect(() => {
setTimeout(() => setCounterSecond(counterSecond + 1), 1000);
setTimeout(() => setCounter(counter - 1), 1000);
}, [counterSecond , counter]);
const handletimer = () => {
setTime(counterSecond);
};
return (
<div className="App">
<div>Countdown: {counterSecond}</div>
<div>Countdown Reverse: {counter}</div>
<div>time: {time} </div>
<button onClick={handletimer}>Submit</button>
</div>
);
}
The best way is to add a state variable representing the status of work. ie: 'working', 'paused' and toggle it.
Also, you need to unsubscribe from timeout to avoid state updates if the component get unmounted.
here is an example where you can stop and resume the timers:
export default function App() {
const [counterSecond, setCounterSecond] = React.useState(0);
const [counter, setCounter] = React.useState(120);
const [time, setTime] = React.useState("");
const [status, setStatus] = React.useState("working");
React.useEffect(() => {
let secondCounterId;
let counterId;
if (status === "working") {
secondCounterId = setTimeout(
() => setCounterSecond(counterSecond + 1),
1000
);
counterId = setTimeout(() => setCounter(counter - 1), 1000);
}
return () => {
clearTimeout(counterId);
clearTimeout(secondCounterId);
};
}, [counterSecond, counter, status]);
const handletimer = () => {
setTime(counterSecond);
};
const stopTimers = () => {
setStatus("paused");
};
const resume = () => {
setStatus("working");
};
return (
<div className="App">
<div>Countdown: {counterSecond}</div>
<div>Countdown Reverse: {counter}</div>
<div>time: {time} </div>
<button onClick={handletimer}>Submit</button>
<button onClick={stopTimers}>Stop</button>
<button onClick={resume}>resume</button>
</div>
);
}
And a working codesandbox
You can create a timerRunning (boolean) variable to check if the timer should run in the useEffect() like this:
const [timerRunning, setTimerRunning] = React.useState(true);
React.useEffect(() => {
if (timerRunning) {
setTimeout(() => setCounterSecond(counterSecond + 1), 1000);
setTimeout(() => setCounter(counter - 1), 1000);
}
}, [counterSecond , counter, timerRunning]);
Then toggle the timerRunning in the handletimer:
const handletimer = () => {
setTimerRunning(false);
// ... other logic
};
The reason time is running because after each render useEffect() will be called.Hence the time. So to correct it, you can set like if "time" is in initialstate then do those functionality otherwise not. So after rendering time will be set set to new time and problem will be solved.

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);

Resources