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

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.

Related

How to run a series of Count Down Clock

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?

Calcul with useState in a map function javascript react

Hello guys I have an array like this :
[
{
"name": "test",
"amount": 794.651786,
"id": "60477897fd230655b337a1e6"
},
{
"name": "test2",
"amount": 10.80918,
"id": "60477bfbfd230655b337a1e9"
}
]
And i wan't to make the total of every amount.
I tried by using the useState hook like this :
const [total, setTotal] = useState(Number);
array.map((item) => {
setTotal(total + item.amount);
});
but it doesn't seems to work as expected.
You could use the reduce method, see docs.
setTotal(array.reduce((sum, item) => sum + item.amount, 0))
I invite you to read this JavaScript: Difference between .forEach() and .map() as well. You should never use .map like this. For this use case, use .forEach instead.
You would want to update the state with the minimum calls needed.
so first, I would do it like this:
let _total = 0;
array.forEach((item) => {
_total += item.amount;
});
setTotal(_total);
That said, You would want to only execute this if array has changed. Assuming array is a prop, this can be done easily with useEffect hook:
useEffect(()=>{
let _total = 0;
array.forEach((item) => {
_total += item.amount;
});
setTotal(_total);
},[array]);
Hope this helps you get a full picture of what the best practice would be. Also you can check out the rules of hooks to get a better understanding on where is best to call setState
My comment wasn't addressed but I'm going to add an answer which addresses my concern - total shouldn't be state at all.
total most likely isn't state - it's computed state - i.e. it's derived from other state and/or props.
If that's the case (99% that it is) it's not correct to set total as state, that just makes for more code and more complicated debugging:
Examples:
When the source of data is a prop:
const Cart = ({someItemsInMyCart}) => {
const total = useMemo(() => someItemsInMyCart.reduce((acc,item) => acc+item.amount,0),[someItemsInMyCart]);
return (/* some JSX */);
}
When the source of data is state:
const Cart = () => {
const [items,setItems] = useState([]);
const total = useMemo(() => items.reduce((acc,item) => acc+item.amount,0),[items]);
return (/* some JSX */);
}
You can write those two examples above and completely leave out the useMemo, which is just a perf optimization, because reducing an array in that manner is pretty darn fast unless you're dealing with 1000s of items.
Try this way
const [total, setTotal] = useState(0);
array.map((item) => {
setTotal(prevCount => prevCount + item.amount);
});

React / javascript addition to array not changing array lenght [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I'm learning react native and I'm doing a small app that regists the time of sleep for each day.
I'm using the useEffect() to trigger some modifications of values showed on screen, one of those is the average time average() that I update inside that useEffect:
const [updateAction, setUpdateAction] = useState(false);
useEffect(() => {
console.log("lenght:" + registSleep.length);
var registed = false;
if (!isNaN(enteredHours)) {
for (var i = 0; i < registSleep.length; i++) {
if (
registSleep[i].day === selectedDay &&
registSleep[i].month === selectedMonth &&
registSleep[i].year === selectedYear
) {
registed = true;
registSleep[i].hours = enteredHours;
}
}
if (!registed) {
var newReg = {
day: selectedDay,
month: selectedMonth,
year: selectedYear,
hours: enteredHours,
};
setNewRegist((prevReg) => [
...prevReg,
newReg,
]);
}
if (registSleep.length != 0) {
average();
}
}
console.log("2. lenght:" + registSleep.length);
setviewInfoAction(!viewInfoAction);
}, [updateAction]);
To debug, as you can see I print to console the lenght before I add a new value to the array of regists setNewRegist(...) and as far as I know it should be printing lenght: 0 and then 2. lenght: 1 but instead it prints lenght: 0 and then 2. lenght: 0 and on the next trigger lenght: 1 and then 2. lenght: 1.
Why the array is not updating on addition?
I'm assuming setNewRegist is useState Hook, where it's value is registSleep
const [registSleep, setNewRegist] = useState([ ... ])
Two reasons why it's not working. useState Hook is asynchronous, the logic will not stop for the logic inside setState.
setNewRegist( ... update registSleep)
console.log(registSleep) // will run before setState finishes
However even it did finish in time, registSleep was already set at a fixed value, so it will not change unless the component is rerendered, which is what setState does, to trigger the component to rerender.
//I am considering this
const [ registSleep , setNewRegist ] = useState([])
setNewRegist is async function, so next statment will execute first in your case console.log so it won't have updated registSleep .
So How to check right?
Tada.... !!! You can check each update of registSleep via useEffect like this :
useEffect(() => {
// Here you can get updated length
// as soon as registSleep updates
console.log(registSleep.length);
},[registSleep]) // <-- watch for any update on `registSleep`

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])

Why is my value from state hook different than what I'm logging in my useEffect?

If useEffect runs after the render phase, why is my value in useEffect less than what is being shown in the return?
I have a component that will update value whenever my counter changes on cleanup
const [value, setValue] = useState(0);
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log(`value = ${value} from effect`);
return () => {
setValue(v => v + 1);
console.log(`value = ${value} from cleanup`);
};
}, [counter]);
return (
<div>
<button onClick={() => setCounter(v => v + 1)}>Increment Counter</button>
<p>value: {value}</p>
</div>
);
On first increment, the value in my return will be 1, but my useEffect will log it as 0. Why would those values be different and why would the useEffect not log 1 as well? This component doesn't have a real purpose, just something I'm experimenting with
Let's step through your component and look at what's actually happening. I will mark value and counter with their respective render to show which version is used like value_1 for first rerender.
Mount
value_0 = 0, counter_0 = 0.
Effect is run. log value_0. register cleanup using value_0.
return JSX using value_0.
Event: user clicks increment. counter is set to 1.
1. rerender triggered by setCounter
value_1 = 0, counter_1 = 1.
Compare deps, [0] elements differ from [1]. Cleanup is run. log value_0. set value to 1. Effect is run. log value_1. register cleanup using value_1.
return JSX using value_1.
2. rerender triggered by setValue
value_2 = 1, counter_2 = 1.
Compare deps, [1] elements match [1].
return JSX using value_2.
-
You will notice that value and counter are constants for each render. They are defined as const after all. Whenever cleanup is run, the function that is executed is the one returned by the last render, which used its own version of value.
The effect is never run on the last update where value is finally up to date because the deps haven't changed.
You can avoid this kind of confusion by using the react-hooks/exhaustive-deps lint rule. It will stop you accidentally using out-of-date values in your effects, callbacks or memoised values.

Categories