React Redux update item quantity (more than just one increment) - javascript

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

Related

Delete an object from object array {}

I'm trying to build a 'to-do' list for a task. Initial state must have the structure shown on code. I'm new to coding and cannot figure out how to delete an object from an object array.
I have tried using the .pop() and .filter() methods but they are not accepted because the object array is an object of objects and not an actual array. I also tried to find the index and do delete state.data[index] but the console sends an error message "cannot update component while rendering other component". Rest of the code works fine when I don't include the handleDeleteClick() function and remove the deleteItem reducer. Here's the code:
//the following creates an item component for each item in the 'to do' list
import React, {useState} from 'react';
import { useDispatch } from 'react-redux';
import { editItem, deleteItem, completedItem } from '../store/todoSlice';
const TodoItem = ({ id, content, completed }) => {
const dispatch = useDispatch();
//initialising state for items that the user wants to edit
const [edit, setEdit] = useState(false);
const [newItem, setNewItem] = useState(content);
const [finishedItem, setFinishedItem]= useState(false);
//function to call deleteItem reducer
const handleDeleteClick = () => {
dispatch(deleteItem({id, content}))
}
//function to call editItem reducer
const onSubmit = (event) => {
event.preventDefault();
dispatch(
editItem({id, newItem, completed}));
//setting edit and finished state back to null
setNewItem("");
setEdit(false);
setFinishedItem(false);
};
//function to call completedItem reducer
const completedTask = (event) => {
event.preventDefault();
dispatch(
completedItem({id, content, completed})
);
setFinishedItem(true);
};
//if edit state is true, return <input> element to edit the item requested
if(edit){
return (
<form>
<input id="editInput" value={newItem} onChange= {(e) => setNewItem(e.target.value)} placeholder="Edit your item"/>
<button onClick = {onSubmit} type='submit' id="submitButton">ADD</button>
</form>
)
}
//if edit state is false and finishedItem is true, return same list and add an id to completed button
if(!edit && finishedItem) {
return(
<div id="itemSection">
<li id="item">{content}
<button onClick= {handleDeleteClick(content)} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask} id="completed">COMPLETED</button>
</li>
</div>
)
}
//else, return <ul> element for each 'todo' item with 3 buttons to edit, delete or complete task
return (
<div id="itemSection">
<li id="item">{content}
<button onClick={handleDeleteClick()} className="function">DELETE</button>
<button onClick={() => setEdit(true)} className="function"> EDIT</button>
<button onClick={completedTask}>COMPLETED</button>
</li>
</div>
);
};
export default TodoItem;
//the following creates state slice for the todos object array
import { createSlice } from "#reduxjs/toolkit";
export const toDoSlice = createSlice({
name: "todos",
//set initial state of object array
initialState: {
nextId: 2,
data:
{
1: {
content: 'Content 1',
completed: false
}
}
},
reducers: {
//function to add item to object array
addItem: (state, action) => {
state.data =
{
...state.data,
[state.nextId]: {
content: action.payload.content,
completed: false
}
}
state.nextId += 1;
},
//function to delete item from object array
deleteItem: (state, action) => {
const index= action.payload.id;
delete state.data[index];
},
//function to edit item from object array
editItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.newItem,
completed: false
}
}
},
//function to complete item from object array
completedItem: (state, action) => {
state.data =
{
...state.data,
[action.payload.id]: {
content: action.payload.content,
completed: true
}
}
}
}
});
export const {addItem, editItem, deleteItem, completedItem} =
toDoSlice.actions;
export default toDoSlice.reducer;
The problem with your example is that you're setting up data as an object. You should not do that, unless you have a good reason to, which doesn't seem to be the case.
Instead of:
createSlice({
//...
initialState: {
nextId: 2,
data: { // 👈 object
1: {
content: 'Content 1',
completed: false
}
}
}
// ...
})
you should use:
createSlice({
// ...
initialState: {
nextId: 2,
data: [{ // 👈 array
content: 'Content 1',
completed: false
}]
}
// ...
})
Now data has all the array methods available, including .filter(). 1
If, for whatever reason, you want to keep data as an object, you could use
delete data[key]
where key is the object property you want to delete. (e.g: if you want to delete 1, use delete state.data.1 or delete state.data['1']).
But my strong advice is to change data to an array.
Notes:
1 - Note you will need to modify all your reducers to deal with the array. For example:
{
reducers: {
addItem: (state, action) => {
state.data.push({
content: action.payload.content,
completed: false
})
}
}
}
Most likely, you won't need state.nextId anymore. That's the advantage of dealing with arrays, you don't need to know what key/index you're assigning to when you add an item.
You will likely need to add an unique identifier to each item (e.g: an id) so you can find it by that id when you want to delete or modify it.

Can I make a multiple createSlice at the createSlice with addSlice and subSlice?

So I was just wondering to create a multiple list of crud by adding a multiple list in a button.
So basically for example I have 4 players and 4 players have each group they called red-blue-yellow-green... and they have teammates something like that...or maybe I can increase it into thousand or billion times.
so the code should be like this
import { createSlice } from '#reduxjs/toolkit'
let initialValues = [{
id:1,
name:"Johnny",
username:"Bravo"
}]
let crudSlice = createSlice({
name:"cruds",
initialState:{ value: initialValues},
reducers:{
addPlayer:( state, action) => {
state.value.push(action.payload)
}
}
})
const initialCrud = [{
id:1,
name:"Johnny",
crudSlicer:crudSlice,
crudReducer:crudSlice.reducer,
crudActions: crudSlice.actions.addPlayer,
}]
export const usersSlice = createSlice({
name:"groups",
initialState:{ value: initialCrud },
reducers:{
multipleUser: ( state, action) => {
state.value.push(action.payload)
},
subtractUser: ( state,action ) => {
state.value = state.value.filter(
(group) => group.id !== action.payload.id
)
},
...
}
})
export const {
// addUser,
// deleteUser,
// updateUser,
multipleUser,
subtractUser
} = usersSlice.actions;
export default usersSlice.reducer
but since I can't export the reducers and also the initialState of each group so how do I do it? Although it works by just adding new createSlice but I don't know how to change it without exporting.
Here is the sample of how I will create a new createSlice
const crudList = useSelector((state) => state.users.value)
let crudSlice = createSlice({
name:"cruds",
initialState:{ value : [{
id:1,
name:"Johnny",
username:"Bravo"
}] },
reducers:{
addPlayer:(state,action) => {
state.value.push(action.payload)
}
}
})
....
<button onClick={() => {
dispatch(multipleUser({
id:crudList.length + 1,
name:`Johnny${crudList.length + 1}`,
crudSlicer:crudSlice,
crudReducer:crudSlice.reducer,
crudActions: crudSlice.actions.addPlayer,
}))
}}>
Add Crud
</button>
So is there any possible way I can get the states of the initialstate of each group players in crud without exporting? or anything else? I know this kinda stupid to do but I kinda like the idea of it.
Anyways I kinda get this error on it..I don't understand what is non-serializable thing when I clicked the button.
A non-serializable value was detected in an action, in the path: `payload.crudSlicer.reducer`. Value: Æ’ (state, action) {
if (!_reducer) _reducer = buildReducer();
return _reducer(state, action);
}
Take a look at the logic that dispatched this action:

How to set state within a reducer

I have an array of product objects inside my reducer and I have an empty array of brand as well. I want to add all the unique brands from my products object array into my brands array in my reducer, is there any way I can do that?
My Reducer:
import * as actionTypes from './shop-types';
const INITIAL_STATE = {
products: [
{
id: 1,
brand:"DNMX"
},
{
id: 2,
brand: "Aeropostale",
},
{
id: 3,
brand: "AJIO",
},
{
id: 4,
brand: "Nike",
},
],
cart: [],
brands: [], //<---- I want to add all the unique brands inside this array
currentProduct: null,
};
const shopReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.ADD_TO_CART:
const product = state.products.find(
(product) => product.id === action.payload.id
);
if (product !== null) {
return {
...state,
cart: state.cart.concat(product),
};
} else {
return null;
}
default:
return state;
}
};
export default shopReducer;
Brands is essentially derived data, meaning it's based off, or reliant on other data. Because of this, you don't actually need to set it in state, and instead rather, just derive it.
I'd normally recommend using Redux Toolkit as it's far simpler, but as you're using old-school Redux, I'd recommend using a library called Reselect. It's a library for creating memoized selectors that you can consume in your component.
For your example, I'd try something like:
// import the createSelector function
// we'll use to make a selector
import { createSelector } from "reselect"
// create a "getter". this is just a simplified
// way of accessing state
const selectBrands = (state) => state.products
// create the selector. this particular selector
// just looks at `products` in state (from your
// getter), and filters out duplicate values
// and returns a unique list
const uniqueBrands = createSelector(selectBrands, (items) =>
items.filter(
(item, idx, arr) =>
arr.findIndex((brand) => brand.name === item.name) === idx
)
)
Then in your component code, you can access this in mapStateToProps:
const mapStateToProps = (state) => ({
uniqueBrands: uniqueBrands(state),
})
This is currently untested, but should be what you're looking for.
It's not really clear if you mean unique brands by cart or products, but it shouldn't change the patterns you'll use to solve this.
First assuming the product list isn't changing, you can simply add them as part of the initial state.
const ALL_PRODUCTS = [
{ id: 1, brand:"DNMX" },
{ id: 2, brand: "Aeropostale" },
{ id: 3, brand: "AJIO" },
{ id: 4, brand: "Nike" }
];
const distinct = (array) => Array.from(new Set((array) => array.map((x) => x.brand)).entries());
const ALL_BRANDS = distinct(ALL_PRODUCTS.map((x) => x.brand));
const INITIAL_STATE = {
products: ALL_PRODUCTS,
cart: [],
brands: ALL_BRANDS,
currentProduct: null,
};
If you will have an action that will add a new products and the brands have to reflect that you just apply the above logic during the state updates for the products.
const reducer = (state = INITIAL_STATE, action) => {
switch(action.type) {
case (action.type === "ADD_PRODUCT") {
const products = [...state.products, action.product];
return {
...state,
products,
brands: distinct(products.map((x) => x.brand))
}
}
}
return state;
};
Now for the idiomatic way. You might note that brands can be considered derived from the products collection. Meaning we probably dont even need it in state since we can use something called a selector to create derived values from our state which can greatly simplify our reducer/structure logic.
// we dont need brands in here anymore, we can derive.
const INITIAL_STATE = {
products: ALL_PRODUCTS,
cart: [],
currentProduct: null;
};
const selectAllBrands = (state) => {
return distinct(state.products.map((x) => x.brand))
};
Now when we add/remove/edit new products we no longer need to update the brand slice. It will be derived from the current state of products. On top of all of that, you can compose selectors just like you can with reducers to get some really complex logic without mucking up your store.
const selectCart = (state) => state.cart;
const selectAllBrands = (state) => {...see above}
const selectTopBrandInCart = (state) => {
const cart = selectCart(state);
const brands = selectBrands(brands);
// find most popular brand in cart and return it.
};
I would highly recommend you check out reselect to help build composable and performant selectors.

How do I update the quantity without duplicate in redux?

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

After removing an item from an array it removes all duplicates entries - Redux

I am making a shopping cart app with react-redux , and i am facing a problem. Every item in my list has an ID , but when I add 2 items with the same ID and then I suddenly removes them from the cart , all the items with the same id (aka duplicates are removed.Code below.
Reducer.js
const reducer = (state = { items: [] }, { type, payload }) => {
console.log(payload);
switch (type) {
case "ADD_ITEM":
return {
...state,
items: [...state.items, payload]
};
case "REMOVE_ITEM":
const index = payload.index;
return {
...state,
items: state.items.filter(({ id }) => id !== payload.id)
};
case "DUPLICATE_ITEM":
return {
...state
};
default:
return state;
}
};
Action.js
const addItem = item => ({
type: "ADD_ITEM",
payload: item
});
const removeItem = (item) => ({
type: "REMOVE_ITEM",
payload: item
});
const duplicateItem = () => ({
type: "DUPLICATE_ITEM"
});
export { addItem, removeItem, duplicateItem };
Item.js
const items = [
{
id: 1,
name: "projectile",
price: "150$",
units: 0
},
{
id: 2,
name: "cute cat",
price: "1250$",
units: 0
}]
Sounds to me like the issue is more of a design problem i.e. rather than adding N of the same item to the cart, you probably want to increase the units/qty of the item if it already exists? Then when you remove, decrease the units/qty until it's reduced to 1 and then remove.
If you want the behaviour to work as is you would need to change how you remove from the cart e.g.
remove by index rather than by ID
create surrogate IDs for items when they are added to the cart and then remove by this

Categories

Resources