How to run a series of Count Down Clock - javascript

I am trying to run multiple count-down from an array, e.g., [11, 12, 13, 14, 15, 16]. What I would like to achieve is that the first time set count down time to 11, when it reach 0, the timer is set to 12 and then count down to 0. After that, reset to 13 and count down, then reset to 14 and count down, etc.
However, my code can only count down from 11 to 0, and then stop. For loop seems not working and never put the second item 12 into the timer. I later found out it is because the retun inside for loop break it outside of the loop. I wonder if there are some smart ways to avoid return in a For-Loop? Or, how to use Return in For-Loop without breaking out of the loop?
(I have another counter there called TotalTime which I inteneded to count all the total time it takes, ie, 11+12+13+14,etc)
The Timer and Display Screen:
import {View, Text, StyleSheet, FlatList} from 'react-native';
import PlaySounds from './PlaySounds';
const CustomTimer = ({time, length}) => {
const [seconds, setSeconds] = useState(time);
const [totalTime, setTotalTime] = useState(0);
useEffect(() => {
if (seconds > 0 )
{ const interval = setInterval(() => {
setSeconds(seconds => seconds - 1);
setTotalTime(seconds=>seconds + 1)
}, 1000);
return () => clearInterval(interval);
}}, [seconds])
return (
<View>
<Text style={{fontSize:20}}> Count Down: {seconds} sec</Text>
<Text style={{fontSize:20}}> Total Time: {totalTime} sec</Text>
</View>
)}
export default CustomTimer;
=====================
import React, {useEffect, useState} from 'react';
import {SafeAreaView,View,Button,ScrollView, Text, StyleSheet} from 'react-native';
import CustomTimer from '../component/CustomTimer';
const BrewScreen = () => {
const timeArray= [11, 12, 13, 14, 15, 16]
const length = timeArray.length
const countDownArray=() =>{
for (let i=0; i<length; i++) {
return(<CustomTimer time={timeArray[i]} length={length}/>)
}
}
return (
<>
<ScrollView>
{countDownArray()}
</ScrollView>
</>
)
}

The issue seems to be that you return inside the for loop.
This means that during the first iteration of the for loop in the countDownArray function you return the Timer element. When you do this, the function will exit and so the loop will not continue.
What you would need instead to achieve your desired behaviour is a different way of creating the Timer elements. This will likely require a callback function in the Timer element. This can be used to update the state of the BrewScreen, and update the Timer displayed.

I finally solve it, not using loops but the all mighty useEffect:
const CustomTimer = ({time}) => {
const [seconds, setSeconds] = useState(time[0]);
const [count, setCount] = useState(1)
const [totalTime, setTotalTime] = useState(0);
useEffect(() => {
if (seconds >= 0 )
{ const interval = setInterval(() => {
setSeconds(seconds => seconds - 1);
setTotalTime(seconds=> seconds + 1)
console.log('seconds ', seconds, ' totalTime ', totalTime)
}, 1000);
return () => clearInterval(interval);}
else if (count<(time.length)) {
setCount(count => count+1)
setSeconds(time[count])}
else return
}
)
return (
<View>
<Text style={{fontSize:20}}> Count Down: {seconds} sec</Text>
<Text style={{fontSize:20}}> Total Time: {totalTime} sec</Text>
{seconds===3?<PlaySounds/>:null}
</View>
)}
Having Return inside For Loop will break out of the loop
JS will execute all the i in one go so you need to add Promise with Async/Await pair
Return won't break ForEach loop
However, Async does not work in ForEach loop
useEffect automatically run the loop with proper support of time Interval
In my case where I want to run a count-down clock, setInterval is must preferred to setCountDown, since setInterval renew EVERY milisecond you specified.
Remeber to clean up your interval with clearInterval
There is one thing I don't understand: The initial value of count has to be 1 instead of 0. It seems like count is forced into initial value the first time it is read. If I initialized it as 0, the first number in array will be executed twice.
Also, the final number displayed in Count Down is -1.... I know it is because this is how the useEffect is stopped, but wonder how to avoid showing a negative number?

Related

setTimeout Function Not Removing Characters Like A Queue

This is a simple for loop that runs 80 times but only every 100 ms. Each time it runs, it pushes a Y coordinate for a character into the stillChars state array. The stillChars state array is mapped through to create a sequence of text elements directly underneath eachother.
See this video to see the animation
const StillChars = () => {
const [stillChars, setStillChars] = useState([]);
function addChar() {
for (let i = 0; i < 80; i++) {
setTimeout(() => {
setStillChars((pS) => [
...pS,
{
y: 4 - 0.1 * 1 * i,
death: setTimeout(() => {
setStillChars((pS) => pS.filter((c, idx) => idx !== i));
}, 2000),
},
]);
}, i * 100);
}
}
useEffect(() => {
addChar();
}, []);
return (
<>
{stillChars.map((c, i) => {
return <StillChar key={i} position={[0, c.y, 0]} i={i} />;
})}
</>
);
};
Code Sandbox
Desired behavior: The death: setTimeout function should remove the characters like a queue instead of whatever is going on in the video/codesandbox.
Your main mistake is with:
setStillChars((pS) => pS.filter((c, idx) => idx !== i))
The problem is that this sits within a setTimeout() so the above line runs at different times for each iteration that your for loop does. When it runs the first time, you update your state to remove an item, which ends up causing the elements in your array that were positioned after the removed item to shift back an index. Eventually, you'll be trying to remove values for i that no longer exist in your state array because they've all shifted back to lower indexes. One fix is to instead associate the index with the object itself by creating an id. This way, you're no longer relying on the position to work out which object to remove, but instead, are using the object's id, which won't change when you filter out items from your state:
{
id: i, // add an `id`
y: 4 - 0.1 * 1 * i,
death: setTimeout(() => {
setStillChars((pS) => pS.filter(c => c.id !== i)); // remove the item based on the `id`
}, 2000),
},
Along with this change, you should now change the key within your .map() to use the object's id and not its index, otherwise, the character won't update in your UI if you're using the same index to represent different objects:
return <StillChar key={c.id} position={[0, c.y, 0]} i={i} />;
As also highlighted by #KcH in the question comments, you should also remove your timeouts when your component unmounts. You can do this by returning a cleanup function from your useEffect() that calls clearTimeout() for each timeout that you create. In order to reference each timeout, you would need to store this in an array somewhere, which you can do by creating a ref with useRef(). You may also consider looking into using setInterval() instead of a for loop.

Restart timer component with new duration after onComplete event in React

Using react-countdown-circle-timer to render a countdown timer. Trying to create an event handler that changes the duration of the timer each time the timer reaches zero.
The clock initially runs down from 5 seconds, when it reaches zero I change the state, then use this to change the new value assigned to the clock.
For a more practical example. When the clock initially runs to zero, I change my stage state to 2, setting the timer to 6 seconds. When this clocks runs down, I change my stage state to 3, setting the timer to 7 seconds and so on, until I stop at stage 5
This is my code:
const [ timer, setTimer ] = useState(5)
const [stage, setStage] = useState(1)
const timerMap = {
2: 6,
3: 7,
4: 8,
5: 9
}
const changeTimeHandler = (timer, setTimer) => {
setStage(stage+1)
setTimer(timerMap[stage])
return { delay:1 }
}
console.log(timer)
return (
<div className="App">
<div className="timer-wrapper">
<CountdownCircleTimer
isPlaying
duration={timer}
colors={["#004777", "#F7B801", "#A30000", "#A30000"]}
colorsTime={[10, 6, 3, 0]}
onComplete={() => (changeTimeHandler(timer, setTimer))}
size={100}
>
{renderTime}
</CountdownCircleTimer>
</div>
</div>
);
When I run my code I get the error:
Warning: Received NaN for the 'strokeDashoffset' attribute. If this is expected, cast the value to a string.
Need help figuring out how to build the logic to get the desired effect.
You don't have any condition to stop the iteration.
A simple solution would probably be:
const changeTimeHandler = (timer, setTimer) => {
// -- stop if no more stages available
if( !timerMap[stage] ){
return { shouldRepeat: false } // <-- Note: Maybe not 100% correct. I don't know this libraray.
}
setStage(stage+1)
setTimer(timerMap[stage])
return { delay:1 }
}

"variable" is read only

I have the following code and I am getting error of "count" is read-only:
import React, {useState} from 'react';
import {View, Text, StyleSheet, Button} from 'react-native';
const CounterScreen = () => {
const [count, setCount] = useState(0);
return (
<View>
<Button onPress={() => setCount(count++)} title="Increase" />
<Button onPress={() => setCount(count--)} title="Decrease" />
<Text>Current Counter: {count}</Text>
</View>
);
};
const styles = StyleSheet.create({});
export default CounterScreen;
But if I use setCount(count + 1) instead of setCount(count++), it works perfectly. What is the difference here between count + 1 and count++
The important thing to remember is that you can change the value of a variable declared as a const, but you cannot reassign it. Here is a good blog post about this:
https://mathiasbynens.be/notes/es6-const
setCount(count+1) is taking the current value of the variable count, adding 1 to it and then passing this to our setter function, 'setCount' to update our state. We are in no way reassigning the 'count' variable. React is updating the value of 'count' under the hood using the setter function.
What you are doing with count++ is attempting to re-assign the count variable, not just change the value of the 'count' variable.
count++ is shorthand for count = count + 1; you are actually changing the value of a variable. not just reading its value.
count++ is similar as count = count + 1
means it's changing the variable itself
and you know const are read-only after initialization that is why you doing setCount() to update the value.
and count + 1 is not updating itself.
const [count, setCount] = useState(0);
Your count in here ↑ is read only, if you want to change it's value have to call setCount()
Like setCount(num)
so if num= 10 then equal to setCount(10)
So that setCount(count + 1) could work, count+1 is a number
But why setCount(count++) couldn't work is because
count++
Equal to
count=count+1; //It is changing the value of count directly!
setCount(count++)
setCount(count=count+1) //So count++ is changing the value of count directly, not through setCount, couldn't work.
It couldn't work and may error for "count" is read-only
This is try to change count value without calling setCount in setCount

change state with conditionals inside a UseEffect (simple counter with hooks)

I'm trying to build a simple percentage counter from 0 to 100 that is updating itself using SetInterval() inside useEffect(). I can get the counter work but I would like to restart the counter once it reaches the 100%. This is my code:
const [percentage, setPercentage]=useState(0);
useEffect(() => {
const intervalId= setInterval(() => {
let per = percentage=> percentage+1
if(per>=100){
per=0
}
setPercentage(per)
}, 100);
return () => {
}
}, [])
Inside the console I can see the state is increasing but it will ignore the if statement to reset the state to 0 once it reaches 100. How can I tackle this knowing that if conditionals are not great with hooks setState?
Check the percentage instead of per. per is of type function and will never be greater or equal to 100, percentage is the value that will reach 100.
This will make your effect depend on percentage which you have avoided by using the function. In this situation, if I still don't want to add that dependency, then I might use a reducer instead to manage that state. This way I don't need to depend on percentage inside of the useEffect.
const reducer = (state, action) => state >= 100 ? 0 : state + 1;
The way you would do this while keeping useState is by moving the check into the state setting function.
setPercentage(percentage => percentage >= 100 ? 0 : percentage + 1);
This might be the quicker option for you. Notice how similar these are, in the end useState is implemented using the useReducer code path as far as I know.
Below should work.
const [percentage, setPercentage] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setPercentage(prev => prev >= 100 ? 0 : prev + 1);
}, 100);
return () => clearInterval(intervalId);
}, [])
Note that per in your code is a function and therefore cannot used to compare against numbers. You may also want to clear the interval in destruction of component.

why is useEffect rendering unexpected values?

I am trying to create a scoreboard for a quiz application. After answering a question the index is updated. Here is the code for the component.
export const ScoreBoard = ({ result, index }) => {
const [score, setScore] = useState(0)
const [total, setTotal] = useState(0)
const [rightAns, setRight] = useState(0)
useEffect(() => {
if(result === true ) {
setRight(rightAns + 1)
setTotal(total + 1)
}
if(result === false) {
setTotal(total + 1)
}
setScore(right/total)
}, [index]);
return (
<>
<div>{score}</div>
<div>{rightAns}</div>
<div>{total}</div>
</>
)
}
When it first renders the values are
score = NaN
rightAns = 0
total = 0
After clicking on one of the corrects answers the values update to
score = NaN
rightAns = 1
total = 1
and then finally after one more answer (with a false value) it updates to
score = 1
rightAns = 1
total = 2
Score is no longer NaN but it is still displaying an incorrect value. After those three renders the application begins updating the score to a lagging value.
score = 0.5
rightAns = 2
total = 3
What is going on during the first 3 renders and how do I fix it?
You shouldn't be storing the score in state at all, because it can be calculated based on other states.
All the state change calls are asynchronous and the values of state don't change until a rerender occurs, which means you are still accessing the old values.
export const ScoreBoard = ({ result, index }) => {
const [total, setTotal] = useState(0)
const [rightAns, setRight] = useState(0)
useEffect(() => {
if(result === true ) {
setRight(rightAns + 1)
setTotal(total + 1)
}
if(result === false) {
setTotal(total + 1)
}
}, [index]);
const score = right/total
return (
<>
<div>{score}</div>
<div>{rightAns}</div>
<div>{total}</div>
</>
)
}
Simpler and following the React guidelines about the single "source of truth".
Your problem is that calling setState doesn't change the state immediately - it waits for code to finish and renders the component again with the new state. You rely on total changing when calculating score, so it doesn't work.
There are multiple approaches to solve this problem - in my opinion score shouldn't be state, but a value computed from total and rightAns when you need it.
All of your set... functions are asynchronous and do not update the value immediately. So when you first render, you call setScore(right/total) with right=0 and total=0, so you get NaN as a result for score. All your other problems are related to the same problem of setScore using the wrong values.
One way to solve this problem is to remove score from state and add it to the return like this:
return (
<>
{total > 0 && <div>{right/total}</div>}
<div>{rightAns}</div>
<div>{total}</div>
</>
)
You also can simplify your useEffect:
useEffect(() => {
setTotal(total + 1);
if(result === true ) setRight(rightAns + 1);
}, [index]);
With how you have it set up currently, you'd need to make sure that you are updating result before index. Because it seems like the useEffect is creating a closure around a previous result and will mess up from that. Here's showing that it does work, you just need to make sure that result and index are updated at the right times.
If you don't want to calculate the score every render (i.e. it's an expensive calculation) you can useMemo or useEffect as I have shown in the stackblitz.
https://stackblitz.com/edit/react-fughgt
Although there are many other ways to improve how you work with hooks. One is to make sure to pay attention to the eslint react-hooks/exhaustive-deps rule as it will forcefully show you all the little bugs that can end up happening due to how closures work.
In this instance, you can easily calculate score based on total and rightAns. And total is essentially just index + 1.
I'd also modify the use effect as it is right now to use setState as a callback to get rid of a lot of dependency issues in it:
useEffect(() => {
if (result === true) {
setRight(rightAns => rightAns + 1);
setTotal(total => total + 1);
}
if (result === false) {
setTotal(total => total + 1);
}
}, [index]);
useEffect(()=>{
setScore(rightAns / total ||0);
},[rightAns,total])

Categories