SetInterval on mount for a set duration - javascript

I have gone through some Q&As here but havent been able to understand what I am doing wrong. The following component prints 0s in console and does not update the DOM as expected.
const NotifPopup = ({ notif, index, closeHandler }) => {
const [timer, setTimer] = useState(0);
useEffect(() => {
const timerRef = setInterval(() => {
if (timer === 3) {
clearInterval(timerRef);
closeHandler(index);
} else {
console.log("timer", timer);
setTimer(timer + 1);
}
}, 1000);
}, []); // only run on mount
return (<div className="notifPopup">
<span className=""></span>
<p>{notif.message}</p>
<span className="absolute bottom-2 right-8 text-xs text-oldLace">{`closing in ${timer}s`}</span>
</div>);
};
Why is the setInterval printing a stream of 0s in console and not updating the DOM?

You are loggin, comparing, and setting a stale value due to closures.
See more use cases in a related question.
useEffect(() => {
// timerRef from useRef
timerRef.current = setInterval(() => {
setTimer((prevTimer) => prevTimer + 1);
}, 1000);
}, []);
useEffect(() => {
console.log("timer", timer);
if (timer === 3) {
clearInterval(timerRef.current);
}
}, [timer]);

Check out the code for useInterval in react-use. Inspecting the different hooks in this package can greatly improve your hooks understanding.
import { useEffect, useRef } from 'react';
const useInterval = (callback: Function, delay?: number | null) => {
const savedCallback = useRef<Function>(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
};
export default useInterval;
And the usage as described in the docs:
import * as React from 'react';
import {useInterval} from 'react-use';
const Demo = () => {
const [count, setCount] = React.useState(0);
const [delay, setDelay] = React.useState(1000);
const [isRunning, toggleIsRunning] = useBoolean(true);
useInterval(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);
return (
<div>
<div>
delay: <input value={delay} onChange={event => setDelay(Number(event.target.value))} />
</div>
<h1>count: {count}</h1>
<div>
<button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start'}</button>
</div>
</div>
);
};
To start the interval on mount simply change the value of isRunning on mount:
useMount(()=>{
toggleIsRunning(true);
});

Related

Unable to get updated props in setInterval React functional component

I am unable to get the updated prop in setInterval inside Component1, it gives me the old value
following is the code I am using:
import { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counter);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return <h1>Component1 count: {counter}</h1>;
};
In this code inside Component1, the counter's value is updated after every second on the browser, but on the console.log inside setInterval, I am always getting the initial value not the updated one.
I also get a solution which looks like this
import { useState, useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
const counterRef = useRef(null);
counterRef.current = counter;
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counterRef.current);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return <h1>Component1 count: {counter}</h1>;
};
But in this solution, I have to use extra memory space as I am creating a ref and assigning value to it.
Is there any better solution there to get updated value inside setInterval to this the correct way to do it.
You can try this one in scenario, where you face closure behavior of JavaScript,
useEffect(() => {
const intervalId = setInterval(() => {
console.log('count is', counter);
}, 1000);
return () => clearInterval(intervalId);
}, [counter]);
But in your scenario this will be more suitable in Component1,
useEffect(() => {
console.log(counter);
}, [counter]);
In this case : on first render you set an Interval that print 0 on Console every second
useEffect(() => {
const intervalID = setInterval(() => {
console.log(counter);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
I Edit your codes :
import { useState, useEffect } from "react";
export default function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => {
setCounter((counter) => counter + 1);
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Component1 counter={counter} />
</div>
);
}
const Component1 = ({ counter }) => {
useEffect(() => {
console.log(counter);
}, [counter]);
return <h1>Component1 count: {counter}</h1>;
};
on this case in component1 : when value of counter changed, console.log print new value
useEffect(() => {
console.log(counter);
}, [counter]);
read about useEffect
If you want to execute a method when a property changes. you can use useEffect and you must pass that property in closure like below code :
useEffect(() => {
console.log(counter);
}, [counter]);
I pass counter to useEffect and when it changes, useEffect run console.log with new value

react-native state is read-only

Following Component in react-native:
import { useEffect, useState } from 'react'
let startValue = null // only using this to restart the counter from resetTimer() (other better approaches?)
export const NewTimer = () => {
const [seconds, setSeconds] = useState(startValue)
const formatedTime = () => {
return [pad(parseInt(seconds / 60)), pad(seconds % 60)].join(':')
}
useEffect(() => {
const timer = setInterval(() => setSeconds(++seconds), 1000) // I guess this line triggers the error
return () => {
clearInterval(timer)
}
}, [])
return formatedTime
}
const pad = (num) => {
return num.toString().length > 1 ? num : `0${num}`
}
export const resetTimer = () => {
startValue = 0
}
results in Uncaught Error: "seconds" is read-only
Can anyone point where the mistake is? Thx!
when you do ++seconds, you are attempting to mutate seconds this render, which isn't allowed. I would use the setState callback to get the current value, and just do seconds + 1, which will accomplish the same thing:
useEffect(() => {
const timer = setInterval(() => setSeconds((seconds) => seconds + 1), 1000)
return () => {
clearInterval(timer)
}
}, [])

How can I Pause/Stop and Interval on a function that is executed with useEffect()?

I am making a countdown timer script in react.js.
Before the timer starts, a 3 or 5 seconds countdown is displayed, data for both countdown becomes from another component.
I am trying to stop/pause main countdown with a button.
My problem is how can I control data from a function that is executed by useEffect()?
I am trying crating a state for the btn, but the scope for the state is the useEffect
import TimerForm from './components/TimerForm';
const CountDown = (props) => {
const [timeLeft, setTimeLeft] = useState({
minutes: 0,
seconds: 0
});
const [counter, setCounter] = useState();
const [startTimer, setStartTimer] = useState(false);
const [result, setResult] = useState();
let Interval;
useEffect(() => {
let count = null;
if(startTimer){
if(counter === 0){
relog(result);
clearInterval(count);
return
}
count = setInterval(() => {
setCounter((prevcounter) => prevcounter - 1);
}, 1000);
return () => clearInterval(count);
} else {
clearInterval(count);
}
}, [startTimer, counter]);
const relog = useCallback((ForTime) => {
console.log(testing);
Interval = setInterval(() => {
setTimeLeft({
seconds: ForTime % 60,
minutes: Math.floor(ForTime / 60)% 60
});
if(ForTime === 0){
clearInterval(Interval);
return;
}
ForTime--;
},1000);
setStartTimer(false);
},[]);
const timerSettings = (data) => {
setCounter(data.counter);
setResult(data.result);
setStartTimer(true);
}
return (
<div>
<section>
<TimerForm onTimerSettings={timerSettings} />
<span>{counter}</span>
<div className="TimerClock">
<span>{timeLeft.minutes}</span><span>:</span><span>{timeLeft.seconds}</span>
</div>
<div>
<button type="button" className="btn btn-stop">Pause Button</button>
</div>
</section>
</div>
)
};
export default CountDown;```
create a useInterval hook used by useEffect() and useRef().
/**
*
* #param {Function} callback
* #param {number|null} delay, stopped when delay is null
*/
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
use useInterval:
function App() {
const [delay, setDelay] = useState(null);
const [counter, setCounter] = useState(0);
useInterval(() => {
setCounter((counter) => counter - 1);
}, delay);
const handleChangeDelay = () => {
// stopped when delay is null
setDelay(delay === null ? 1000 : null);
};
return (
<div className="App">
<p>
<button onClick={handleChangeDelay}>trigger interval countdown</button>
</p>
<p>{counter}</p>
</div>
);
}
You can store the interval timer by using useRef
const countRef = useRef(null);
useEffect(() => {
if (startTimer) {
if (counter === 0) {
relog(result);
clearInterval(countRef.current);
return;
}
countRef.current = setInterval(() => {
setCounter((prevcounter) => prevcounter - 1);
}, 1000);
return () => clearInterval(countRef.current);
} else {
clearInterval(countRef.current);
}
}, [startTimer, counter]);
And you can get the timer from the useEffect scope
return (
...
<button type="button" className="btn btn-stop" onClick={()=>{clearInterval(countRef.current)}}>Pause Button</button>
...
)

How to use setInterval with react useEffect hook correctly?

I tried to create a simple timer app with ReactJS and found the below code on the internet.
Does the function that we passed to the useEffect will execute with the dependency change or does it recreates with every dependency change and then execute?
Also I console log the return function of the useEffect and it runs with every render. Does it run only when the component unmount? or with every render?
import { useEffect, useState } from "react";
const App = () => {
const [isActive, setIsActive] = React.useState(false);
const [isPaused, setIsPaused] = React.useState(true);
const [time, setTime] = React.useState(0);
React.useEffect(() => {
let interval = null;
if (isActive && isPaused === false) {
interval = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval);
}
return () => {
console.log("cleanup");
clearInterval(interval);
};
}, [isActive, isPaused]);
const handleStart = () => {
setIsActive(true);
setIsPaused(false);
};
const handlePauseResume = () => {
setIsPaused(!isPaused);
};
const handleReset = () => {
setIsActive(false);
setTime(0);
};
return (
<div className="stop-watch">
{time}
<button onClick={handleStart}>start</button>
<button onClick={handlePauseResume}>pause</button>
<button onClick={handleReset}>clear</button>
</div>
);
};
export default App;
The code inside the useEffect hook will run every time a dependency value has been changed. In your case whenever isActive or isPaused changes state.
This means that the reference to the interval will be lost, as the interval variable is redefined.
To keep a steady reference, use the useRef hook to have the reference persist throughout state changes.
const App = () => {
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(true);
const [time, setTime] = useState(0);
const interval = useRef(null)
useEffect(() => {
if (isActive && !isPaused) {
interval.current = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval.current);
interval.current = null;
}
return () => {
clearInterval(interval.current);
};
}, [isActive, isPaused])
...
}

not able to clear Interval using variable which is outside the react hook

I am trying to access clearTimerInterval in clearTimer method but getting undefined , got the waring variable from inside React Hook will be lost after each render. in below code useEffect hook called once once then how variable clearTimerInterval got undefined?
function Child(props) {
let [timerCount, setTimer] = useState(0);
var clearTimerInterval;
useEffect(() => {
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);
function clearTimer() {
clearInterval(clearTimerInterval);
}
return (
<div>
<div>Timer {timer}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
);
}
export default React.memo(Child);
If you need to save variables across re-renders use useRef which in this case acts like a class instance field, also note that mutations to refs does not trigger a re-render.
This will give you the ability to clear the interval from outside of useEffect
function Child(props) {
let [timerCount, setTimer] = useState(0)
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimer(prevState => prevState + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
function clearTimer() {
clearInterval(intervalRef.current)
intervalRef.current = null
}
return (
<div>
<div>Timer {timerCount}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
)
}
Try defining the variable inside of the hook.
useEffect(() => {
var clearTimerInterval;
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);

Categories

Resources