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])
Related
I'm not sure if this is related to React or just JavaScript.
I'm building a simple voting app. You can add some options and vote +1 for each option.
My App has options as state like below. storedOptions is from localStorage.
function App() {
const [options, setOptions] = useState(
storedOptions ? JSON.parse(storedOptions) : []
);
And handleVote increases count by 1 for the given option.
const handleVote = useCallback((option) => {
setOptions((options) => {
console.log("previous: ", options);
let updatedOptions = [...options];
console.log(updatedOptions); // THIS PART IS STRANGE
const index = updatedOptions.indexOf(option);
console.log(index);
updatedOptions[index] = { ...option, count: option.count + 1 }; // Change reference of the given option only
console.log("new: ", updatedOptions);
updatedOptions = sortByValue(updatedOptions, "count"); // I think this is not related to my problem though, this is why I declared updatedOptions with 'let'. sortByValue function returns new array.
localStorage.setItem(OPTIONS_KEY, JSON.stringify(updatedOptions)); // I'm working with localStorage too, you can ignore this
return updatedOptions;
}, []);
});
But when I voted for an option, it didn't work as it supposed to be. So I logged them out like above and found out that console.log(updatedOptions)(second log) already reflected future operation(increasing count).
Shouldn't count be 0 at that moment? why 1 already?
screeen record of the issue: https://streamable.com/ofn42v
it is working fine in local but once deployed to production(vercel), it is not working. i have tried sooo many different things like having a separate state in cart, useEffect with totalQuantity in dependency array and nothing seems to work. Ideally when the totalQuantity inside the context is updated, the components using it should rerender as mentioned in react doc which is happening from n to 2 except for 1. can someone please help :(
my code for the cart icon in nav bar:
function Cart(props) {
const { enableCart, totalQuantity } = useContext(AppContext);
return (
<>
{enableCart ? (
<Link href="/cart" passHref>
<a aria-label="Shopping cart" title="Shopping cart">
<Badge count={totalQuantity} offset={[0, 5]}>
<ShoppingCartIcon className="w-7 h-7" />
</Badge>
</a>
</Link>
) : null}
</>
);
}
Update quantity - code in appContext:
import { useCookies } from "react-cookie";
export const AppProvider = (props) => {
const [cartItems, updateCart] = useState([]);
const [totalQuantity, setTotalQuantity] = useState(0);
const [cookies, setCookie] = useCookies(["cart"]);
const cookieCart = cookies.cart;
useEffect(() => {
cartOperations();
}, []);
const calculateAmountQuantity = (items) => {
let totalCount = 0;
let totalPrice = 0;
items.forEach((item) => {
totalCount += item.quantity;
totalPrice += item.price * item.quantity;
setTotalAmount(totalPrice);
setTotalQuantity(totalCount);
});
};
const cartOperations = async (items) => {
if (items !== undefined) {
updateCart([...items]);
calculateAmountQuantity(items);
} else if (cookieCart !== undefined) {
updateCart([...cookieCart]);
calculateAmountQuantity(cookieCart);
} else {
updateCart([]);
setTotalAmount(0);
setTotalQuantity(0);
}
};
const addItem = (item) => {
let items = cartItems;
let existingItem;
if (items) existingItem = items.find((i) => i.id === item.id);
if (!existingItem) {
items = [
...(items || []),
Object.assign({}, item, {
quantity: 1,
}),
];
updateCart([...items]);
setTotalAmount(totalAmount + item.price * 1);
setTotalQuantity(totalQuantity + 1);
} else {
const index = items.findIndex((i) => i.id === item.id);
items[index] = Object.assign({}, item, {
quantity: existingItem.quantity + 1,
});
updateCart([...items]);
setTotalAmount(totalAmount + existingItem.price);
setTotalQuantity(totalQuantity + 1);
}
saveCartToCookie(items);
saveCartToStrapi(items);
};
i am storing the cart content in cookie.
code for AppContext is here in github, full nav bar code
Live url: https://sunfabb.com
Goto Products, add few items to cart, then try removing one by one from the cart page. (i have enabled react profiler in prod as well)
EDIT: This issue is completely specific to antd library. I was able to debug further based on the below 2 answers and there is nothing wrong with react context or re-render. i tried using a custom badge for cart and it is working perfectly fine. Yet to fix the antd issue though. I can go with custom one, but antd's badge is better with some animations.
As pointed out by #hackape, when setting the value of state to something that depends on the previous value of that state, you should pass a function to the setState instead of a value.
So instead of setTotalQuantity(totalQuantity + 1);, you should say setTotalQuantity(previousQuantity => previousQuantity + 1);.
This is the safe way of doing that, so for example if we are trying to do it twice simultaneously, they both get taken into account, instead of both using the same initial totalQuantity.
Other thing that I would think about changing is that you are setting those quantities and amounts in multiple places, and relying on the previous value. So if it goes out of sync once, it's out of sync also on the next action, and so on.
You could use the useEffect hook for this. Every time the cartItems change, calculate those values again, and do that based only on the new cartItems array, not on the old values.
Something like this for example:
useEffect(() => {
setTotalAmount(cartItems.reduce((total, currentItem) => total + (currentItem.price * currentItem.quantity), 0));
setTotalQuantity(cartItems.reduce((total, currentItem) => total + currentItem.quantity, 0));
}, [cartItems]);
Or if you prefer calling it like you do now, I would still replace the value with the reduce from my example, so it get's calculated based on the whole cart instead of previous value.
A shopping cart is usually something that contains less than 100 entries, so there is really no need to worry about the performance.
From looking at the renders and from seeing that after a refresh the cart shows as empty as should be, it's probably a lifecycle issue.
I'd suggest creating another useEffect hook that listens to totalQuantity or totalAmount (logically the bigger of the two though by the state values it looks either should be fine) and in the hook call change the cart icon based on the updated sum
EDIT:
misread your inter-component imports, because Cart (from components/index/nav.js) should listen for changes from the context.provider you would use a context.consumer on Cart with the totalQuantity value (not just with importing the variable from the context as that rides on the application rendering from other reasons)
see example in consumer docs and in this thread, and check this GitHub issues page for other's detailed journey while encountering this issue more directly
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);
});
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.
I'm writing a React component that takes a total number of reviews as a prop. When the number of reviews is 0, I want to render an element stating
<div>No reviews yet.</div>
Otherwise, I render elements containing review data. Here is the component and its context:
const Stats = ({good, neutral, bad, totalReviews}) => {
if ({totalReviews} === 0)
return <div>No reviews yet.</div>
else {
return (
<div>
<Stat text="Total: " amount={totalReviews} />
</div>
);
}
}
const App = () => {
const [good, setGood] = useState(0);
const [neutral, setNeutral] = useState(0);
const [bad, setBad] = useState(0);
let totalReviews = good + neutral + bad;
return (
<div>
<Stats totalReviews={totalReviews} />
</div>
)
}
I have used the debugger command to check in Chrome's developer console the values of each variable. It shows that totalReviews = 0. The variables good, neutral, and bad all also = 0.
I've also used console.log(totalReviews).
0 is displayed by the console.log. How come my program enters the second conditional as if totalReviews isn't 0?
if (totalReviews === 0)
You only wrap js statements in curly braces inside jsx, but your if statement is just regular js.
Problem with your if condition.
it should be if (totalReviews === 0) or if (totalReviews == 0) to avoid strongly type conversation check.
You have added {} inside if condtion which is not a stadard way