cart count is not getting updated properly - javascript

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

Related

Removing an specific item from an array in React

i have this code https://stackblitz.com/edit/react-wc2ons?file=src%2FSection.js
I have sections, and i can add items to those sections. How can i delete some item? I tried
const removeItem = (i) => {
setSections((section) => {
const itemTarget = section.items[i];
const filtered = section.items.filter((item) => item !== itemTarget);
return {
...section,
items: filtered,
};
});
};
But for some reason it doesn't work
The removeItem callback prop you pass into the Section component is the way to go and you should get rid of passing setSections down to it as well.
removeItem={(i) => removeItem(index, i)}
Child components shouldn't do parent's work so you had it right at first, I'm going to help you implement that since I can already see the removeItem handler being there in the App component.
removeItem has already all the info you need, I'm going to rename the arguments so it's more clear.
const removeItem = (sectionIndex, index) => {
const newSections = sections.slice();
const newItems = newSections[sectionIndex].items.slice();
newItems.splice(index, 1);
newSections[sectionIndex].items = newItems;
setSections(newSections);
};
Then get rid of removeItem implementation in the Section component and destructure it from the props.
You are using setSections, but you return a single section instead of an array of sections. You probably need something like this:
// using the `section` variable from the upper scope
const removeItem = (i) => {
setSections((sections) => {
const itemTarget = section.items[i];
const filtered = section.items.filter((item) => item !== itemTarget);
const newSections = [...sections];
newSections[section.id] = {
...section,
items: filtered,
};
return newSections;
});
};
A few tips (you don't have to follow them): TypeScript can prevent such mistakes and give useful error messages. Immer.js can make writing such code simpler.
Your problem is that section is an array. So you are currently accessing the undefined property items on it. You would have to change your function to something like this
const removeItem = (i) => {
setSections((section) /* aqui vc tinha chamado de prev*/ => {
const itemTarget = section[i].items[j];
const filtered = section[i].items.filter((item) => item !== itemTarget);
return [...section, {
...section[i],
items: filtered,
}]
});
};
where i is the section in question and j is the item you want to delete.
here is a crude solution to your problem (i noticed other bugs in the code but this solves your issue with removing items at least), but i would separate the sections and items into separate components that in turn has its own states.
There you can add/remove items withing its parent section much more easily.
Now we have to work around this by looking for which section the code wants to remove the current item in.
https://stackblitz.com/edit/react-xxbvp1?file=src%2FSection.js

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 useState pre populated from previous component render

I'm trying to create a Quiz component rendering one Question at time and changing it when the user chooses one of the alternatives.
However, every time it renders the next Question it has already the chosenOption variable set from the previous Question. This happens because before I change the Question, I set the new state of the current Question with that chosenOption and strangely(to me) this is already set when the next Question component is rendered.
For me, the setChosenOption would set only for the current Question and when the Quiz renders the next Question its chosenOption would be null initially. I may be missing something from how functional components render... So why is it happening?
Thanks in advance!
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = React.useState(0)
const [answers, updateAnswers] = React.useState({})
const numberOfQuestions = questions.length
const onChoosenOptionCallbackHandler = ({ hasChosenCorrectOption, chosenOption}) => {
updateAnswers(prevAnswers => ({...prevAnswers, [currentQuestion]: hasChosenCorrectOption }))
setCurrentQuestion(currentQuestion + 1)
}
return (
<QuizBackground>
<QuizContainer>
<Question
question={questions[currentQuestion]}
index={currentQuestion}
numberOfQuestions={numberOfQuestions}
onChoosenOptionCallback={onChoosenOptionCallbackHandler}
/>
</QuizContainer>
</QuizBackground>
)
}
Here, apart from the first Question, the 'Chosen Option: ' log always show the chosenOption from the previous Question rendered and not null.
const Question = ({ question, index, numberOfQuestions, onChoosenOptionCallback }) => {
const [chosenOption, setChosenOption] = React.useState(null)
console.log('Chosen Option: ', chosenOption)
const hasChosenCorrectOption = chosenOption !== null ? (chosenOption == answer) : false
const selectOption = (optionIndex) => {
setChosenOption(optionIndex)
console.log('SELECTED: ', optionIndex, hasChosenCorrectOption, chosenOption)
onChoosenOptionCallback({ hasChosenCorrectOption, optionIndex })
}
return (
{/* I removed other parts not relevant. The RadioOption goes inside a map() from the question alternatives */}
<RadioOption
questionName={questionName}
option={option}
chosen={chosenOption === index}
onSelect={() => selectOption(index)}
key={index}
/>
)
}
Your issue is a result of not assigning keys to your Question components, that are being rendered using a map function.
The omission of proper keys (i.e. a unique property of each element in the rendered array) results in all sorts of weird behaviours, such as what you were describing.
The reason for that is that React uses these indices to optimize, by re-rendering only the components whose props were changed. Without the keys the whole process isn't working properly.

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

Shopping cart / adding multiple items to cart

I am trying to create a cart with React js and Redux and I have one problem.
Tried many ways but I keep failing that when I add multiple items (food/drink) to the list then everything seems to be working, but then when I want for example add additional drink to the existing choice my list gets overwritten. Here is the code I have it now:
const addItemToCart = item => {
if (cartItems.length) {
cartItems.forEach(itemToCheck => {
if (item.name === itemToCheck.name) {
addCountToItem({ ...itemToCheck, count: itemToCheck.count + 1 });
} else if (item.name !== itemToCheck.name) {
addToCart({ ...item, count: 1 });
}
});
} else if (cartItems.length === 0) {
addToCart({ ...item, count: 1 });
}
};
Idea is that I can have multiple items on the list and unlimited number of same items within the list. So basically, I should be able to have 5 pizzas of the same type, 3 beers of different type etc.
I guess like any other cart. Thanks in advance.
update:
Here the code for addCountToItem. I deleted it but it was going something in this direction
state.cartItems[findIndex(...)] = data.cartItem
a basic way to solve your problem is
`let index=cartItem.findIndex(temp=>temp.name===item.name);
if(index>-1){
cartItem[index].count++;
}
else{
cartItem.append({...item, count: 1 })
}`
try not to mutate cartItem object
We need too see to all the related code to successfully answer.
Here I give sample example, updatedCartItems keeps the updated cart, you can do whatever you want. In general, this type of manipulation must be in the cart reducer, but you didn't post the the reducer code.
const addItemToCart = item => {
let updatedCartItems = [...cartItems];
updatedItemIndex = updatedCartItems.findIndex(
item => item.name === itemToCheck.name // better to check with some kind of id if exists
);
if (updatedItemIndex < 0) {
updatedCartItems.push({ ...item, count: 1 });
} else {
const updatedItem = {
...updatedCartItems[updatedItemIndex]
};
updatedItem.count++;
updatedCartItems[updatedItemIndex] = updatedItem;
}
//updatedCartItems => the new cart
};
for shopping card, we need to have cartItems property as array in our state, and every time we click on the addToCart button, we will push that item to that array and then we render that array in the cartDropdown component or the checkout page.
since you are able to add single item to the cart, it means that you have correct set up for redux. in order to add same item to the cart more than once, we just need to write a simple utility function.
here is the utility function:
export const addItemToCart = (cartItems, cartItemToAdd) => {
//find(condition) finds the first item in the array based on the condition.
const existingCartItem = cartItems.find(item => item.id === cartItemToAdd.id);
if (existingCartItem) {
//in order for change detection to trigger we have to rerender
//otherwise our quantity property will not be updated
//map will return a new array
//we need to return new versions of our state so that our component know to re render
//here we update the quantity property
return cartItems.map(item =>
item.id === cartItemToAdd.id
? { ...cartItemToAdd, quantity: item.quantity + 1 }
: item
);
}
//when you first time add a new item, sine exixtingCartItem will be falsy, it will pass the first if block and will come here
//quantity property gets attached the first time around since this if block wont run when it is a new item.
//in the beginning cartItems array is empty. every time you add a new item to this array, it will add "quantity:1" to this item object.
return [...cartItems, { ...cartItemToAdd, quantity: 1 }];
};
here is the action to add item to the cart
export const CartActionTypes = {
ADD_ITEM: "ADD_ITEM",
};
export const addItem = item => ({
type: CartActionTypes.ADD_ITEM,
payload: item
});
since you are able to add single item to the cart, it means that you have correct set up for redux. you need to dispatch this to the reducer in the component that you render addToCart button. here is the cart reducer where the case is CartActionTypes.ADD_ITEM.
import { addItemToCart } from "./cart.utils";
case CartActionTypes.ADD_ITEM:
return {
...state,
cartItems: addItemToCart(state.cartItems, action.payload)
};

Categories