start timer when user click button, React native - javascript

I created a function in which when user click start button timer will start, but it's not working. Can someone tell me why it's not working? please
that's the function I created
const [time,setTime] = useState(0)
const timeout = setInterval(() => {
if (time !== 60) {
setTime(prevState => prevState + 1);
}
}, 1000);
console.log(timeout);
return () => {
if (time == 60) {
clearTimeout(timeout);
}
};```

You could declare the Timer State as 60 instead of 0 and
const [state,updateState] = useState({timer:60})
then call this in updateState: ({timer: timer - 1})

To answer your question:
why is my code not working?
Your state timer starts out being 0 and will therefore never reach inside the if statement.
As Matt U pointed out you most likely want to use setInterval since it runs the function you pass at every X milliseconds (1000 in your case) until you stop it.
See the following for more information regarding that:
setTimeout: https://www.w3schools.com/jsref/met_win_settimeout.asp
setInterval: https://www.w3schools.com/jsref/met_win_setinterval.asp
What yesIamFaded answered should do the job in your use case, though it would be better to make use of updateState's argument prevState (or whatever you want to call it). updateState will receive the previous value and use that to compute a new value.
const [state, updateState] = useState({ timer: 60 })
const interval = setInterval(() => {
if (state.timer > 0) {
updateState(prevState => { timer: prevState.timer - 1 });
}
}, 1000);
You can read more about functional updates here:
https://reactjs.org/docs/hooks-reference.html#functional-updates
And lastly, you should clear the timeout and/or interval once you don't need it anymore using either clearTimeout() or clearInterval().
See the following for more information here:
clearTimeout: https://www.w3schools.com/jsref/met_win_cleartimeout.asp
clearInterval: https://www.w3schools.com/jsref/met_win_clearinterval.asp
P.S.
If your timer state isn't coupled with any other state I wouldn't put it into an object. Instead I would do the following:
const [timer, setTimer] = useState(60)
const interval = setInterval(() => {
if (timer > 0) {
setTimer(prevTimer => prevTimer - 1 );
}
}, 1000);
That way you won't have an unnecessary object.

Related

React, can't access updated value of state variable inside function passed to setInterval() in useEffect()

I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);

How to trigger a function at regular intervals of time using hooks and when certain criteria is met I want to clear the time interval?

I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)

How to update state object within setInterval? [Hooks]

I am wondering if anyone has experience merging a state object within a setInterval() function. After trying a few things, I ended up with the solution below, but would appreciate any additional input / tips on how to do this.
Some context: My codebase started growing, and now I have multiple state variables. I am trying to group the ones that are related into a single object to have more control over the number of renders that occur. One of those state variables is updated within a setInterval() function.
I originally had a single state:
const [seconds, setSeconds] = useState(10)
const start = () => {
interval = setInterval(() => {
setSeconds((seconds) => seconds - 1000);
}, 1000);
}
But I am trying to implement something like:
const [timer, setTimer] = useState({ seconds: 10, status: 'initial', count: 0 })
And I need to update the 'seconds' property of this object. First I attempted something like ... setTimer({ ...timer, seconds: timer.seconds - 1000 }); ... which left the interval running, but the 'seconds' were never updated from the subtraction.
Eventually, I implemented the following, which seems to do the trick so far:
const start = () => {
interval = setInterval(() => {
setTimer((timer) => (timer = { ...timer, seconds: timer.seconds - 1000 }));
}, 1000);
}
For example, you can use Immer, like in this article. And you can set your state more easily.
You need to do like this:
const start = () => {
interval = setInterval(() => {
setTimer((timer) => ({ ...timer, seconds: timer.seconds - 1000 }));
}, 1000);
}
Or better would be this:
const updatedTimer = {...timer,seconds:timer.seconds - 1000};
const start = () => {
interval = setInterval(() => {
setTimer(updatedTimer);
}, 1000);

Why useEffect cleanup logging every time?

I'm implementing count down
and use useRef hook to using it when clean setTimeout when the user navigates to the next screen to avoid cancel all subscription warning and it's work!
But I have something weird when count - 1 i can see "hey" in the console! although not cleaning the setTimeOut!!
I don't want to clean it in this case but why should loggin every time count changes!
code snippet
const [seconds, setSeconds] = useState(40);
const countRef = useRef(seconds);
useEffect(() => {
if (seconds > 0) {
countRef.current = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
} else {
setSeconds(0);
}
return () => {
console.log('hey'); // every count down it's appeared
clearTimeout(countRef.current);
};
}, [seconds]);
You see "hey" because you're using seconds as a dependency. So every time seconds changes, the effect must run again leading to the effect's destroy function (the function you returned from the effect) to be invoked.
Instead of having seconds as a dependency, you should instead have setSeconds.
const [seconds, setSeconds] = React.useState(10);
useEffect(() => {
let didUnsub = false;
const id = setInterval(() => {
setSeconds((prev) => {
// while the count is greater than 0, continue to countdown
if (prev > 0) {
return prev - 1;
}
// once count eq 0, stop counting down
clearInterval(id);
didUnsub = true;
return 0;
});
}, 1000);
return () => {
console.log("unmounting");
// if the count didn't unsubscribe by reaching 0, clear the interval
if (!didUnsub) {
console.log("unsubscribing");
clearInterval(id);
}
};
}, [setSeconds]);
If you look at the example below, you'll see that the effect is only run once, when the component is mounted. If you were to cause the component to dismount, the destroy function would be invoked. This is because the setState is a dispatch function and doesn't change between renders, therefor it doesn't cause the effect to continuously be called.
In the example you can click the button to toggle between mounting and dismounting the counter. When you dismount it notice that it logs in the console.
Example: https://codesandbox.io/s/gallant-silence-ui0pv?file=/src/Countdown.js

How to use setInterval() and clearInterval() in the same function

I'm trying to create a function that starts and stops a timer. The starting is always on the click of a button, but the stopping can be due to the timer running down or the function being called again from another function.
This is what I have so far. Works perfect for what you see but I cannot figure out how to incorporate clearInterval() so that it stops when the game is won. The functioning calling timerCountdown is located in a different js file. I've read answers to similar questions but they are all seem to be doing it a little differently to where I can't make it work for my case
I do realize that I need to call clearInterval(count) but I don't know how to incorporate this into the function itself.
const timerCountdown = () => {
let count = setInterval(() => {
let timeLeft = timer.innerHTML
if (timeLeft > 0) {
timer.innerHTML = timeLeft - 1
} else {
gameOverMessage()
}
}, 1000)
}
You need to push the interval id in a global variable. Like that you can use another function to stop the interval when you want.
Like
let intervalId; // define `count` globaly
let timer = document.getElementById('timer')
const timerStop = () => {
clearInterval(intervalId)
}
const timerRestart = () => {
timerStop()
timer.innerHTML = 100;
}
const timerStart = () => {
timerStop(); // Stop timer by security, e.g. if you call `timerStart()` multiple times
intervalId = setInterval(() => {
let timeLeft = timer.innerHTML;
if (+timeLeft > 0) {
timer.innerHTML = +timeLeft - 1
} else {
timerRestart();
gameOverMessage()
}
}, 1000)
}
<div id="timer">100</div>
<div>
<button onclick="timerStart()">Start</button>
<button onclick="timerStop()">Pause</button>
<button onclick="timerRestart()">Restart</button>
</div>
setInterval makes a best effort to space the running of the callback according to the interval you specify. The thing is: in a game, what you actually want is the current state of the world to be printed to the screen in smooth and timely fashion. This is different to the behavior of setInterval, which knows nothing about the screen and is blindly called repeatedly.
For example: if you kick-off setInterval(foo, 100) for your game in a browser tab, and then navigate to another tab in your browser, wait ten seconds and then return to your game, your foo callback will be invoked about a hundred times in rapid succession as the queued callbacks are "drained". It is highly unlikely you want this behavior.
requestAnimationFrame is a better solution for this, because it is only called when (shortly before) your game is rendered - which is what you want.
In the following code a timer object is created by createTimer. The timer has start, stop and toggle methods.
The start method records when it was invoked and triggers requestAnimationFrame, supplying a callback called tick. Every time a tick occurs, we run some logic to see which (if any) callback to invoke.
If the time elapsed is greater than or equal to the duration of the timer, then the onTimeout callback is invoked and the timer is stopped.
If the time elapsed is smaller than the duration, but greater than or equal to the interval period, then we update the lastInterval and invoke the onInterval callback.
Otherwise we simply cue up another tick of the timer.
The stop method simply uses the request animation ID to cancel the timer with cancelAnimationFrame.
function createTimer() {
let rafId = null
function start({duration = 10000, interval = 1000, onInterval, onTimeout, onStop, startTime=performance.now(), lastInterval = startTime}) {
function tick(now=performance.now()) {
const elapsed = now - startTime
if (elapsed >= duration) {
cancelAnimationFrame(rafId)
rafId = null
return onTimeout()
}
if ((now - lastInterval) >= interval) {
lastInterval = now
onInterval({
duration,
elapsed
})
}
rafId = requestAnimationFrame(tick)
}
rafId = requestAnimationFrame(tick)
}
function stop() {
cancelAnimationFrame(rafId)
rafId = null
return onStop()
}
function toggle(...args) {
rafId ? stop() : start(...args)
}
const timer = {
start,
stop,
toggle
}
return timer
}
const timer = createTimer()
const onInterval = ({duration, elapsed})=>console.log(`Remaining: ${((duration - elapsed)/1000).toFixed(0)}`)
const onTimeout = ()=>console.log('Timed out.')
const onStop = ()=>console.log('Manually stopped.')
document.getElementById('btn').addEventListener('click', () => timer.toggle({
onInterval,
onTimeout,
onStop
}))
<button id="btn">Toggle Timer</button>
You could take a global variable intervalId and clear the interval timer, if won or if no time is avaliable.
var intervalId;
const timerCountdown = () => {
intervalId = setInterval(() => {
let timeLeft = timer.innerHTML
if (timeLeft > 0) {
timer.innerHTML = timeLeft - 1
} else {
clearInterval(intervalId);
gameOverMessage();
}
}, 1000)
},
won = () => {
clearInterval(intervalId);
// additional code
};

Categories