Spread operator reflects future operation while in React setState - javascript

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?

Related

How to fill a state with an array

I'm trying to add a list inside a state using the set method, but my state remains empty
App.js
// Starts the screts word game
const startGame = () => {
// pick word and pick category
const { word, category } = pickWordAndCategory();
// create array of letters
let wordLetters = word.split("");
wordLetters = wordLetters.map((l) => l.toLowerCase());
// Fill states
setPickedCategory(category);
setPickedWord(word);
setLettersList(wordLetters);
console.log('wordLetters', wordLetters);
console.log('lettersList', lettersList);
setGameState(stages[1].name);
};
const pickWordAndCategory = () => {
// pick a random category
const categories = Object.keys(words);
const category = categories[Math.floor(Math.random() * Object.keys(categories).length)];
console.log('category', category);
// pick a random word
const word = words[category][Math.floor(Math.random() * words[category].length)]
console.log(word);
return { word, category };
}
Here is the browser log
When looking at the log we find our state empty
When you use setLettersList(...), you are not actually changing lettersList variable itself. Notice how when you declare it, const [lettersList, setLettersList] = useState([]); letterList is actually constant, so that variable can't even be changed. Instead, it tells React to rerun the function component, this time giving lettersList the new value. Therefore, for the updated value, it will only come once const [lettersList, setLettersList] = useState([]); is rerun again from the entire function being rerun again.
If you want to monitor changes to lettersList, you can do:
useEffect(() => {
console.log(lettersList);
}, [lettersList]);
This will print lettersList every time it is updated.
States updates are asynchronous, so when you set a new state value and console.log() right after it, it's going to show the previous value, because the state hasn't been updated yet.
That's why your lettersList value show the old value of the state in the console (empty array).

Nested Object mutates in React

I am in almost desperate need of help. I am a mechanical engineer and I'm doing a type of calculator for my line of work. I have had an issue I've spent weeks on. I can't seem to solve it.
To not bore you with long code I will try to generalise it as much as possible.
I will first present an example code.
Then I will explain the expected behaviour and what is actually happening for me.
Finally I will explain what I have tried so far to solve this issue.
I will add more content at the bottom based on comments to help clarify my question.
CODE EXAMPLE
THE PARENT OBJECT
import {childObject} from "./childObject"
// in my code "childObject" are actually different from each other
const object1 = Object.assign({}, childObject);
const object2 = Object.assign({}, childObject);
const object3 = Object.assign({}, childObject);
const object4 = Object.assign({}, childObject);
const object5 = Object.assign({}, childObject);
const object6 = Object.assign({}, childObject);
const exampleObject = {
name: "foo",
otherInfo: "bar",
nestedObject:{
standardType: [object1, object2, object3],
specialType: [object4, object5, object6]
},
sumfunc(){}
}
THE CHILD OBJECT
export const childObject = {
name: "I'm losing my mind",
value: "" //<-- this will change
otherInfo: "please help me",
sumfunc(){}
}
EXPLAINING
What I am doing is the following:
Searchbar with all types of parentObjects.
Allowing user to select one or multiple of same or different parentObjects.
Storing the copied selection in a redux store.
Displaying the selection, each parentObject as a form. [see picture]
When typing in form the value of the nested object will change
Now... The issue is when I open the searchbar and select the same parentObject, thus copying it, all its values are mutated. As seen in picture above.
WHAT I HAVE TRIED
I have tried to use lodash clone and deepClone on the selected parentObject.
I have tried to use loads clone and deepClone on the selected childObjects.
I have tried, since the object have the same structure, to go through all key value pairs and shallow copy them.
I have tried to not send the parentObject via the searchbar component to the reducer, instead I just send a string and the reducer itself will add the parentObject to the store.
All methods that I've tried have not stopped the mutation. The deepClone method stopped the mutations, but in return the functions in the objects stopped working (maybe I need to bind it somehow?)
MORE CONTENT
The code that updates the value of the nestedObject
const inputsHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const formCopy = Object.assign({}, formEQ);
const inputFieldName = e.target.name;
// if anything other than a empty, number or decimal inputted, then return
const isNum = e.target.value.match(/^(?:\d{1,8}(?:\.\d{0,8})?)?$/);
if (!isNum) return;
// Update priority list to calculate the last updated input
formCopy.priorityList = formCopy.priorityList.sort((a, b) => {
if (a === inputFieldName) return 1;
if (b === inputFieldName) return -1;
else return 0;
});
// Update selected input field
formCopy.inputs[calcmode] = formCopy.inputs[calcmode].map((input) => {
if (input.name === inputFieldName) {
input.value = e.target.value;
}
return input;
});
// If more than two inputs empty do not calculate
const emptyInputs = formCopy.inputs[calcmode].reduce(
(acc, nV) => (nV.value === "" ? (acc += 1) : acc),
0
);
// Calculate the last edited input field
formCopy.inputs[calcmode] = formCopy.inputs[calcmode].map((input) => {
if (input.name === formCopy.priorityList[0] && emptyInputs <= 1) {
const calculatedValue = formCopy.calculate(
formCopy.priorityList[0],
calcmode
);
input.value = Number(calculatedValue).toFixed(2);
}
return input;
});
// Final set hook, now with calculated value
setformEQ({ ...formCopy });
};
Please good people of StackOverFlow... Help me!
Your code has few problems :
you are filtering based on name property of child object and all of them has the same name. Always provide unique id to the objects so that they can be differentiated in easy manner.
Your filter logic is so wrong :
formCopy.inputs[calcmode] = formCopy.inputs[calcmode].map((input) => {
if (input.name === inputFieldName) {
input.value = e.target.value; // < -- Culprit
}
return input;
});
Never mutate inline, always create a new copy.
This is how your code change function should be (I have removed dynamic key selection for clarity) :
const change = (e, id) => {
const inputFieldName = e.target.name;
// local copy of array
const nestedArr = [...qform.nestedObject.standardType];
// finding item to be updated
const index = nestedArr.findIndex((i) => i.id === id);
console.log({ index, inputFieldName, e, id });
if (index !== -1) {
const item = nestedArr[index];
item.value = e.target.value;
nestedArr[index] = item;
// deep copy till k depth where k is the key to be updated
const newObject = {
...qform,
nestedObject: {
...qform.nestedObject,
standardType: [...nestedArr],
},
};
setQform(newObject);
}}
Check this Example : Demo

cart count is not getting updated properly

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

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);
});

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