I am making a shopping cart - onClick I have redux adding items to cartItems array.
In the code below (increment reducer its the last one after add/remove) I am trying to get rid of duplicate values from the cartItems array which holds all the items added to the shopping cart, and display a total number of unique items in the cart with cartIcon: {value: 0} - which is by default 0 (before adding any items).
const initialState = {
cartItems: [],
cartQuantity: 0,
cartIcon: {value: 0},
}
export const addToCartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
add(state, action ) {
const itemIndex = state.cartItems.findIndex(
(props) => props.id === action.payload.id
);
if(itemIndex >= 0){
state.cartItems[itemIndex].cartQuantity += 1;
} else {
const tempProduct = {...action.payload, cartQuantity: 1}
state.cartItems.push(tempProduct);
}
},
remove(state, action) {
const removeItem = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload.id
);
state.cartItems = removeItem;
},
increment: (state) => {
const Items = state.cartItems.filter(
(element, index) => state.cartItems.indexOf(element) === index);
state.value = Items.length;
} // if i just do state.value += 1
// then the value goes up by 1
// but I want to display the amount of unique entries
},
});
Here onClick I am pulling data from the item that was "added" to the cart and additionally trying to increment the cartIcon number by 1 (if the item hasn't been yet added to the array cartItems). The problem could be here? Because the error mentions all the props and data I'm pulling to be rendered into the cart.
const dispatch = useDispatch()
const handleAddToCart = (props) => {
dispatch(add(props));
};
return (<>
<div id={props.id} className='shopitem'>
<img src={props.url} />
<h2>{props.title}</h2>
<p className='boldprice'>${props.price}</p>
<button onClick={() => {
handleAddToCart(props);
dispatch(increment())
}}> ADD TO CART </button>
</div>
</>
)
}
And here I am trying to display the amount of unique items to the shopping cart icon.
const count = useSelector((state) => state.cart.cartIcon.value)
{count}
For some reason I am getting this error. If I just do state.value += 1 it will add +1 to the shopping cart icon, however I only want to display +1 for each unique item.
"Uncaught Error: Objects are not valid as a React child (found: object with keys {id, title, price, url, cartQuantity}). If you meant to render a collection of children, use an array instead."
Please help - I am relatively new to Javascript and programming overall.. I may be making a stupid mistake, so if something is clearly wrong.. then please let me know :)
Related
I have this cart state in which the initial value is an empty array [].
const [cart,setCart] = useState([]);
This is how one of my product object looks like,
{id: 1, name: 'Shoe pair', price: 40}
There is an add to cart button for each product. So when the add to cart button is clicked addToCart function is triggered,
const addToCart = (item) => {
let initialItem = {id: item.id, name: item.name, quantity: 1}
let existingItem = cart.filter(cartItem => item.id === cartItem.id);
if(existingItem.length > 0){
existingItem.quantity = existingItem.quantity + 1;
} else {
setCart(crr => [...crr, initialItem ]);
}
}
What does addToCart do?
As you can see it is simple.First, it creates an object by setting the initial quantity to 1. If already the same product is present in the cart it updates the quantity in the cart product by 1 else the initialItem being added to the cart.
To monitor this occurrence I used useEffect hook,
useEffect(() => {
console.log(cart);
}, [cart]);
My problem is I can't see the cart in the console log when the quantity updates by 1 , But it shows when the initialItem is being pushed to the cart.
First issue: It is find, not filter.
Next issue - modifying item inside of array will not tell React that array is changed, you need to re-set state after existing item update also.
const addToCart = (item) => {
const initialItem = { id: item.id, name: item.name, quantity: 1 };
const existingItem = cart.find((cartItem) => item.id === cartItem.id);
if (existingItem) {
existingItem.quantity += 1;
setCart((curr) => [...curr]);
} else {
setCart((curr) => [...curr, initialItem]);
}
};
The reason your useEffect is not running when you think it should, is because its dependency is not being updated when you think it is. It will run when setCart is called and the reference to cart is updated, and then you will see your console log.
filter returns a new array -- will not mutate the original array.
docs -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
find returns the item by reference, if found -- otherwise returns undeifined.
docs -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
Alternate example:
const addToCart = (item) => {
const existingItem = cart.find(i => i.id === item.id)
const updatedCart = existingItem
? cart.map(i => {
return i.id === item.id ? {...i, quantity: i.quantity + 1} : i
})
: [...cart, item]
}
setCart(updatedCart)
}
My result is seeing item.acf.count as string and not as number. How can I convert this to number?
Here is the function below.
increaseItem = (id) => {
const { cart } = this.state;
cart.forEach(item => {
if (item.id === id) {
item.acf.count += 1
}
})
this.setState({
cart:cart
})
}
My result is seeing item.acf.count as string and not as number. please
how can I convert is to number
You should ensure that the initial item.acf.count state is a number type so this count: item.acf.count + 1 operation works correctly and returns a number type. So long as your state updaters maintain the state invariant of item.acf.count being a number it should work as expected.
Additionally, the increaseItem handler is mutating the cart state and not creating new array/object references.
increaseItem = (id) => {
const { cart } = this.state; // <-- cart is reference to cart state
cart.forEach(item => {
if (item.id === id) {
item.acf.count += 1; // <-- mutation!
}
});
this.setState({
cart: cart // <-- cart state reference back into state
})
}
You should instead shallow copy the cart and then also shallow copy the cart item (any any other nested properties) you want to update. I also suggest using a functional state update so you are correctly updating from the previous state and not any state value closed over in increaseItem callback scope.
increaseItem = (id) => {
this.setState(prevState => ({
cart: prevState.cart.map(item => item.id === id
? {
...item,
acf: {
...item.acf,
count: item.acf.count + 1
}
}
: item),
}));
}
I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:
I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}
Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};
Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };
I have the add and Remove items state and it sends to the cart. Im my cart, when I add, it duplicates. Do I need to create a reducer for this increment so it can update from 1 to 2 instead of duplicating it?
const INITIAL_STATE = []
export const addItem = createAction('ADD_ITEM')
export const increment = createAction('INCREMENT')
export const removeItem = createAction('REMOVE_ITEM')
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => [...state, action.payload],
[removeItem.type]: (state, action) => state.filter(item => item._id !== action.payload._id)
})
Here is my products:
const INITIAL_STATE = []
export const addProducts = createAction('ADD_PRODUCTS')
export const addProduct = createAction('ADD_PRODUCT')
export default createReducer(INITIAL_STATE, {
[addProducts.type]: (state , action) => [...state, action.payload],
[addProduct.type]: (state, action) => [...action.payload]
})
My reducer:
export default configureStore({
reducer: {
products: productsReducer,
cart: cartReducer
}
})
It would be necessary that your reducer verify if the item ID exists or not. Case exists, update the item count. Instead of, create a new item with count equal to 1:
export default createReducer(INITIAL_STATE, {
[addItem.type]: (state, action) => {
const pos = state.map((i) => i._id).indexOf(action.payload._id);
if (pos !== -1) {
state[pos] = { ...action.payload, count: state[pos].count + 1 };
} else {
state.push({ ...action.payload, count: 1 });
}
},
[removeItem.type]: (state, action) =>
state.filter((item) => item._id !== action.payload._id),
});
Quantity
Right now your cart state is saving an array of all of the items that are in the cart. There is no quantity property anywhere. One thing that you could do is add a quantity: 1 property to the item object when you add that item to the cart. But...
Normalizing State
You don't really need to save the while item object because you already have that information in products. All that you really need to know is the id of each item in the cart and its quantity. The most logical data structure for that is a dictionary object where the keys are the item ids and the values are the corresponding quantities.
Since you only need the id I would recommend that your action creators addItem, removeItem and increment should just be a function of the id instead of the whole item. This means that you would call them like dispatch(addItem(5)) and your action.payload would just be the id. The addProduct and addProducts actions still need the whole item object.
An array of products is fine and you can always call state.products.find(item => item._id === someId) to find a product by its id. But a keyed object is better! You want to have the keys be the item ids and the values be the corresponding objects. Redux Toolkit has built-in support for this structure with the createEntityAdapter helper.
Create Slice
There's not any problem with defining your actions though createAction, but it's not needed. You can replace createAction and createReducer with createSlice which combines the functionality of both.
Here's another way of writing your products:
import { createSlice, createEntityAdapter } from "#reduxjs/toolkit";
const productsAdapter = createEntityAdapter({
selectId: (item) => item._id
});
const productsSlice = createSlice({
// this becomes the prefix for the action names
name: "products",
// use the initial state from the adapter
initialState: productsAdapter.getInitialState(),
// your case reducers will create actions automatically
reducers: {
// you can just pass functions from the adapter
addProduct: productsAdapter.addOne,
addProducts: productsAdapter.addMany
// you can also add delete and update actions easily
}
});
export default productsSlice.reducer;
export const { addProduct, addProducts } = productsSlice.actions;
// the adapter also creates selectors
const productSelectors = productsAdapter.getSelectors(
// you need to provide the location of the products relative to the root state
(state) => state.products
);
// you can export the selectors individually
export const {
selectById: selectProductById,
selectAll: selectAllProducts
} = productSelectors;
And your cart:
import {createSlice, createSelector} from "#reduxjs/toolkit";
const cartSlice = createSlice({
name: 'cart',
// an empty dictionary object
initialState: {},
reducers: {
addItem: (state, action) => {
const id = action.payload;
// add to the state with a quantity of 1
state[id] = 1;
// you might want to see if if already exists before adding
},
removeItem: (state, action) => {
const id = action.payload;
// you can just use the delete keyword to remove it from the draft
delete state[id];
},
increment: (state, action) => {
const id = action.payload;
// if you KNOW that the item is already in the state then you can do this
state[id]++;
// but it's safer to do this
// state[id] = (state[id] || 0) + 1
}
}
})
export default cartSlice.reducer;
export const {addItem, removeItem, increment} = cartSlice.actions;
// you can select the data in any format
export const selectCartItems = createSelector(
// only re-calculate when this value changes
state => state.cart,
// reformat into an an array of objects with propeties id and quantity
(cart) => Object.entries(cart).map(([id, quantity]) => ({id, quantity}))
)
// select the quantity for a particular item by id
export const selectQuantityById = (state, id) => state.cart[id]
// you can combine the ids with the products, but
// I actually recommend that you just return the ids and get the
// product data from a Product component like <Product id={5} quantity={2}/>
export const selectCartProducts = createSelector(
// has two input selectors
state => state.cart,
state => state.products,
// combine and reformat into an array of objects
(cart, products) => Object.keys(cart).map(id => ({
// all properties of the product
...products.entries[id],
// added quantity property
quantity: cart[id],
}))
)
I have a site where the user can increase the quantity on the product before adding it to cart. Now if the user decided to go back to the product and add 3 more by increasing the quantity on the product, then adding to cart - how do I update the quantity of the existing product in basket?
At the moment I get duplicates of the product with different quantities depending on what is selected.
Here is the code I have for my reducer:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
items: [],
};
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
// No idea what to do with this..
state.items.filter((pizza) => pizza.name === payload.name);
// This pushes the item fine, but I get multiple of the same item in the cart instead of just updating its quantity
state.items.push(payload);
// state.items.map((pizza) =>
// pizza.name === payload.name
// ? {
// ...pizza,
// quantity: pizza.quantity + payload.quantity,
// }
// : pizza
// );
},
},
});
export const { addToBasket } = basket.actions;
export const basketItems = (state) => state.basket.items;
export default basket.reducer;
The payload is the specific product, it will be an object:
{
name: "product name",
image: "url.jpeg",
price: "14.99"
}
I can not for the life of me figure out what to do here in order not to mutate the state. Nothing works, I feel like I have tried every possible way but clearly I am missing something.
Any help much appreciated!!!
Thanks
You actually have all the code you need, just need it applied correctly. First check that the item is already in the items array or not. If it is already there then copy the existing state and update the matching element. If it is not included then append the new item to the end of the array.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
state = state.items.map((pizza) =>
pizza.name === payload.name
? {
...pizza,
quantity: pizza.quantity + payload.quantity
}
: pizza
);
} else {
state.items.push(payload);
}
},
},
});
With Redux-Toolkit you can mutate the state objects so you can likely simplify this a bit. Instead of mapping a new array and setting it back to state, jut mutate the found object.
const basket = createSlice({
name: "basket",
initialState,
reducers: {
addToBasket: (state, { payload }) => {
const item = state.items.find((pizza) => pizza.name === payload.name);
if (item) {
item.quantity += payload.quantity;
} else {
state.items.push(payload);
}
},
},
});