Modifying state array in reducer - calculation gets executed twice - javascript

const addToCartReducer = (state, action)=>{
console.log('executing reducer')
// const ItemsFilter = (itemIndexer) =>{
// if ((itemIndexer.restaurant == action.item.restaurant) && (itemIndexer.menu == action.item.menu)){
// return itemIndexer
// }
// }
const existingItemIndex = state.items.findIndex(
(item) =>
item.restaurant === action.item.restaurant &&
item.menu === action.item.menu
);
let newState = {...state}
let newStateItems = {...newState.items}
if (action.type === 'ITEM_ADDED'){
console.log('executing ITEM_ADDED')
// let existingItem = newState.items.filter(ItemsFilter)
if (existingItemIndex >= 0){
// newState.items[existingItemIndex].quantity += parseInt(action.item.quantity)
newStateItems[existingItemIndex].quantity += parseInt(action.item.quantity)
newState.items = newStateItems
}
else {
newState.items = newState.items.concat({...action.item, quantity: parseInt(action.item.quantity)})
}
// Update the totalAmount and totalItems using the updated quantity of the item
newState.totalItems = parseInt(newState.totalItems) + parseInt(action.item.quantity);
}
console.log(newState)
return newState
}
This is a code of a React component that provides the state and dispatch functions for updating the state of a shopping cart using the useReducer hook. The state of the shopping cart contains two properties: totalItems, and items. The items property is an array of objects representing the items in the cart, each with the properties: restaurant, menu, quantity, price.
The component exports a provider component, AddToCartProvider, that wraps the children components and provides the state and dispatch functions for adding and removing items from the cart. The dispatch functions are triggered by the addItemToCart and removeItemFromCart handlers.
When an ITEM_ADDED action is dispatched, the addToCartReducer function is executed, and it updates the state by either increasing the quantity of an existing item in the cart (if it already exists) or adding a new item to the cart. The totalAmount and totalItems are also updated accordingly.
When this code tries to update the quantity of an existing item in the
cart (if it already exists), the quantity calculation gets executed
twice with action.item.quantity
Case 1:
newState = {totalItems:1, items:[{restaurant: 'R1', menu: 'F1', quantity: 1}]
//Now, if I dispatch an object {type:'ITEM_ADDED', item:{restaurant:'R1', menu:'F1', quantity:2}}
//I am expecting newState to be changed to
//Expected value for newState
newState = {totalItems:3, items:[{restaurant: 'R1', menu: 'F1', quantity: 3}]
//Actual value for newState
newState = {totalItems:3, items:[{restaurant: 'R1', menu: 'F1', quantity: 5}]

I think it caused by <React.StrictMode>, it only run on development. Test by remove this part on your main or index js. You can read documentation here

Related

For loops and if statements in Hooks?

Is there a way where I can use for loops and if statements without breaking the hook rule? To elaborate, I am currently trying to compare two lists (allData and currentSelection) and if there are similarities, I will add them to another list (favData). However, I am constantly either having visibility issues or errors. If I can get some help, I would much appreciate it!
const [favData, setFavData] = useState([]);
useEffect(() => {
getFilterFavMeal();
}, []);
function getFilterFavMeal() {
allData.forEach((mealList) => {
currentSelection.forEach((mealList2) => {
if (mealList["menu_item"]["menu_item_id"] === mealList2.value) {
// with push, I have visibility issues
// favData.push(mealList);
setFavData(mealList);
}
});
});
setFavData(favData);
}
The set function that useState returns updates the state and schedules a re-render of the component so that the UI can update. It doesn't make sense to call the set function many times in one render.
You also don't want to mutate React state by using functions like push.
Since it looks like favData is deterministic, you can simply remove it from the component state and calculate it in the render loop.
const favData = allData.filter(a => currentSelection.some(c => c.value === a.menu_item.menu_item_id));
Answering your original question, of course you can use loops. As long as you don't mutate the existing state object. And don't set the state more than once per render.
const FF = () => {
const [list, setList] = useState([]);
const addStuffToList = () => {
const tail = Array.from(new Array(3)).map((_e, i) => i);
// Build a new array object and use that when setting state
setList([...list, ...tail]);
}
const forLoop = () => {
const tail = [];
for (let i = 0; i < 4; i++) {
tail.push(i);
}
// Same thing
setList([...list, ...tail]);
}
return ...
};

React: setState doesn't re-render the component

I'm implementing a shopping cart for a ecommerce website. The shopping cart is a state variable shopCart represented by an array of objects. Each object contains information about a product, such as title and price. I am trying to implement a remove button, which is actually doing what is intended from it, which is to remove items from the shopCart state, but the changes are not represented on the screen render. I can empty the cart, but the screen still shows the products. Here is the main code of the shopping cart page:
return (
<div class={styles.container}>
<h1>Product</h1><h1>Quantity</h1><h1>Unit price</h1><h1>Total price</h1><div></div>
{
shopCart.map((product, i, array) => <CartItem key={product.id} product={product} index={i} array={array}/>)
}
</div>
)
And here is the implementation of CartItem.js
const CartItem = (props) => {
let { shopCart, setShopCart } = useContext(Context);
let product = props.product;
// takes the identification of a shopping cart product and removes it from the cart
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart;
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={() => {
decrease(product.id);
}}>Remove</button>
</div>
)
}
Why isn't it rendering the cart correctly, even though the cart items are being removed after each click of the remove button ?
Issue
You are mutating state. You save a reference to state, mutate it, then save it back into state, so the array reference never changes. React uses shallow equality when checking if state or props update.
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart; // <-- Saved state ref
aux.splice(i, 1); // <-- mutation
setShopCart(aux); // <-- Saved ref back to state
}
}
})
}
Solution
The correct way to update arrays in react state is to copy the array elements into a new array reference. This can be easily accomplished by filtering the current cart by item id. I also suggest changing the argument name so it is clearer what it represents.
const decrease = (id) => {
setShopCart(shopCart => shopCart.filter(item => item.id !== id));
}
You're modifying the shopCart (aux is a reference) directly which is both the context and the collection that you're iterating over. You need to make sure you're updating a copy of the shopping cart and resetting the context. Minimally, you can do the following:
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart.slice(); // makes a copy
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
However, I suggest using the approach Drew recommended. The current approach isn't ideal.
The solution is much simpler than you think. You can use array.filter to remove the matching product by id.
const CartItem = (props) => {
const { product} = props;
let { shopCart, setShopCart } = useContext(Context);
// takes the identification of a shopping cart product and removes it from the cart
const handleClick = (e) => {
const filteredShopCart = shopCart.filter(item => item.id !== product.id);
setShopCart(filteredShopCart);
};
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={handleClick}>Remove</button>
</div>
);
};

How to change state inside nested object in React

Hi i have parrent and child component. Parrent component is receiving data from server. This data are saved in state.data. Now, when i do action in child component, it should be call method from parrent controller. This is working now. Problem is inside this method which i am calling. I am receiving id as parameter. This data in parrent have list of items (packages) and every item has id. I need to update only one of them by id (or other way i don't know right way). Please how i can do it? I need to update isOpen state only that one item i open by clicking on button in child component
My method (but i am not sure if i started to do this right way), i stucked on this problem for while:
changeIsOpenState(typeOfPart: Number, id: Number) {
console.log(this.state.data.packages);
const selectedObject = this.state.data.packages.filter((obj) => {
const val= (obj.id === id) ? obj : false;
return val;
});
}
Array of data i want update (isOpen property).
what about immutably? I think u can use dot-prop-immutable package in this way:
const state = {
packages: [
{ isOpen: false, id: 1 },
{ isOpen: false, id: 2 },
{ isOpen: false, id: 3 }
]
};
const index = state.packages.findIndex(obj => obj.id === 3);
const newState = dotProp.set(state, `packages[${index}].isOpen`, true);
you could do it the ol' way :
changeIsOpenState(typeOfPart: Number, id: Number) {
// Copy the packages so you won't mumtate your state directly
const packages = Object.assign({}, ...this.StaticRange.data.packages);
// Get the package to edit and its index in the packages object
let packageIndex;
let packageToEdit;
for(let i = 0; i <= packages.length; i++){
if(packages[i].id === id){
packageIndex = i;
packageToEdit = packages[i];
packageToEdit.isOpen = true
}
}
packages[packageIndex] = packageToEdit;
setState({...this.state, data:{...this.state.data, packages}});
}
I did it like this:
1.I copy current data to another variable
2.Filter data by id
3.Save array key of item with same id
4. Change cloned value with negation
5. save new data to state
changeIsOpenState(typeOfPart: Number, id: Number) {
const subjectDataCopy = cloneDeep(this.state.data);
const keys = [];
subjectDataCopy.packages.filter((obj, key) => {
if (obj.id === id) {
keys.push(key);
}
});
subjectDataCopy.packages[keys[0]].isOpen = !subjectDataCopy.packages[keys[0]].isOpen;
this.setState({data: subjectDataCopy});
}
If there is better option to do this please let me know :)

Problem with setState ReactJS, done in todoList

Hello i have problem about change state after onClick with this function i dont know why this is doesnt work because console.log displayed difference value and i dont know why i cant set the same state.
`doneUndone = (index) => {
console.log(!this.state.scores[index].done)
const test = !this.state.scores[index].done
this.setState({
scores: test,
})
}`
here will be all code of this aplication https://codepen.io/RetupK/pen/xxKmELd?editors=0010
As per your state scores is an array and in your method of done you are assigning Boolean value to it where as it must be an array itself. Because you're using .map() in your render method which only works with array not boolean.
What you need to do is change the done property of particular object in scores and pass the newly updated scores object to setState method and it will work.
doneUndone = (index) => {
this.state.scores[index].done = !this.state.scores[index].done
this.setState({
scores: this.state.scores,
})
}
If you use this.state to get previously done value you might have problems when you fire doneUndone method multiple times (e.g. clicking button few times in a row). That's why I suggest such solution:
doneUndone = index => {
this.setState(state => ({
scores: state.scores.map((score, idx) =>
idx === index ? { ...score, done: !score.done } : score
)
}));
};
The doneUndone method isn't updating the state properly. You can check the method form here.
doneUndone = (index) => {
const score = this.state.scores[index];
const updatedScore = {...score, done: !score.done};
const updatedScores = [...this.state.scores];
updatedScores[index] = updatedScore;
this.setState({
...this.state,
scores: updatedScores
})
}
doneUndone = (index) => {
let modScores = this.state.scores;
modScores[index].done=!this.state.scores[index].done
this.setState({
scores: modScores
})
}
cleaner way to do it

View not updating on state change

So I got a list of buttons that looks like this
The functionality that I aim for is when you press a button its background will change to another color.
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
I use this function to return a list of selected items. Currently I'm only returning one item but I made it an array so I can handle multiple items in the future.
In the render function I have something like this:
<View style={feed_back_page_styles.buttons_wrapper}>
{
feedbackButtons.map((item, i) => (
<TouchableOpacity style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.pressedStyle:feed_back_page_styles.inputStyle}
onPress={()=>this.onButtonPress(item.key)}>
<Text style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.option_text_style_pressed:feed_back_page_styles.option_text_style}>{item.data}</Text>
</TouchableOpacity>
))
}
</View>
feedbackButtons is just an array with a key and text.
The onButtonPress method looks like this:
onButtonPress = (key) =>{
updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems,key);
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
The problem is that the view does not update on state change. When I click the button the state gets updated but the view stays the same.
I think this is wrong
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
Since you are passing the this.state.selectedItems as 1st argument from your onButtonPress, actually its not creating new array, but using the same reference of state and state should not be modified directly, always use setState().
So basically what you are doing is :
const getUpdatedSelectedItemsArray = (id) => {
this.state.selectedItems = []
this.state.selectedItems.push(id);
return selectedItems;
};
Which is completely wrong and might be the actual issue.
what you can do instead is :
const getUpdatedSelectedItemsArray = (selectedItems=[], id) => {
const items = [...selectedItems]
items.push(id);
return items;
};
and then :
onButtonPress = (key) =>{
const updatedItems = getUpdatedSelectedItemsArray(key); // since currently you want to keep only 1 item in the list
/* Incase more than 1 items, you can then use this
const updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems, key);
*/
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
Hope this resolves your issue.
Also, if you can share your component, it can help if there is some other issue with your component like if you are using PureComponent.

Categories