How do I update the quantity without duplicate in redux? - javascript

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

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.

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.

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

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

How can I use Redux to only update one instance of a component?

I'm trying to use Redux to update my Card Component to disable and change colors on click. Redux dispatches the action fine, but it updates all Cards not just the one that was clicked. Each Card has an object associated with it that hold the word and a value. The value is the className I want to use to change the color when clicked
Component
const Card = ({ wordObj, updateClass, isDisabled, cardClass }) => {
const showColor = (e) => {
updateClass(wordObj);
console.log(cardClass)
};
return (
<button
onClick={(e) => showColor()}
disabled={isDisabled}
className={cardClass}>
{wordObj.word}
</button>
);
};
const mapStateToProps = (state) => ({
cardClass: state.game.cardClass,
});
export default connect(mapStateToProps, { updateClass })(Card);
Action
export const updateClass = (obj) => (dispatch) => {
console.log(obj)
dispatch({
type: UPDATE_CARD,
payload: obj,
});
};
Reducer
const initialState = {
words: [],
cardClass: 'card',
isDisabled: false,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_WORDS: {
return {
...state,
words: payload,
};
}
case UPDATE_CARD:
return {
...state,
isDisabled: true,
cardClass: ['card', payload.value].join(' '),
};
default:
return state;
}
}```
All of your card components are consuming the same cardClass field in the state. When you modify it in this line:
cardClass: ['card', payload.value].join(' ')
All cards that are consuming this field have their classes updated. The same occurs to the isDisable field.
You need to create one object for each card in your state. Here is my implementation (was not tested):
const initialState = {
cards: []
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
// create a card object for each word
case SET_WORDS: {
return {
...state,
cards: payload.map(word => {
return { word: word, cardClass: "card", isDisabled: false }
})
};
}
case UPDATE_CARD:
// here i'm using the wordObj.word passed as payload
// to identify the card (i recommend to use an id field)
const cardIndex = state.cards.findIndex(card => card.word === payload.word);
// get the current card
const card = state.cards[cardIndex];
// create an updated card object obeying the immutability principle
const updatedCard = { ...card, isDisabled: true, cardClass: ['card', payload.value].join(' '), }
return {
...state,
cards: [
...state.cards.slice(0, cardIndex), // cards before
updatedCard,
...state.cards.slice(cardIndex + 1) // cards after
]
};
default:
return state;
}
}
Your mapStateToProps selects a string, but said string changes on any updateClass and that causes all your cards to update, because the selection of state.game.cardClass produces a different value, which triggers a new render for the connected component.
Maybe what you want, is something that identifies the selection, i.e. an id for each card, and select with that id in the mapStateToProps to avoid reading the change, because what's happening right now is the following:
Card A[className="card A"] == after dispatch ==> mapStateToProps => [className="card B"]
Card B[className="card A"] => dispatch('B') => mapStateToProps => [className="card B"]
B is updating the state of both A and B, and that's why the extra render occurs

Delete Item from Array (React-Native + Redux)

I have the checklist with users and when I click on the checkbox user should add to the InputField or delete from InputField, if I check to it again.
For now works only ADD.
import ...
export default class NewEvent extends React.Component {
constructor(props) {
super(props);
this.onSelect = this.onSelect.bind(this);
}
onSelect = id => {
addMembers(id) }
findSelectedContacts = (contacts, membersArray) => {
const newArr = [];
contacts.forEach(item => {
if(membersArray.indexOf(item.id.toString()) > -1) {
newArr.push(` ${item.name}`)
}
});
return newArr;
}
render() {
const { navigation, members, location, contacts } = this.props;
const membersArray = members ? members.split(',') : [];
const selectedArray = this.findSelectedContacts(contacts, membersArray)
const inputFill = selectedArray.join().trim();
return (
<InputField
customStyle={[eventStyles.input]}
icon="addGuest"
placeholder="Add guests"
onGetText={texts => {
this.handlerChangeText(texts)
}}
value={inputFill}
/>
);
}
}
Also, I have reducer, which adds guests to input:
import { handleActions } from 'redux-actions';
import * as types from '../actions/actionTypes';
export const initialState = {
members: '',
};
const addMembers = (members, id) => {
const res = members ? `${members},${id}` : `${id}`;
return res;
}
export default handleActions(
{
[types.ADD_GUEST]: (state, action) => ({
...state,
members: addMembers(state.members, action.payload),
}),
},
initialState
);
Please advise, how I can change my reducer? I need to add or delete the user from InputFiled if I click on the ONE checkbox.
Currently, it appears that you are storing the members list as a comma-separated string. A better option would be to store the list as an actual array, and then convert that to a string when it's needed in that format, e.g. rendering.
The reducer for it might look something like this (trying to follow your existing code style:
export const initialState = {
// initialState changed to array
members: [],
};
const addMember = (members, id) => {
// add id to the end of the list
return members.concat(id);
}
const removeMember = (members, id) => {
// return a new list with all values, except the matched id
return members.filter(memberId => memberId !== id);
}
export default handleActions(
{
[types.ADD_GUEST]: (state, action) => ({
...state,
members: addMember(state.members, action.payload),
}),
[types.REMOVE_GUEST]: (state, action) => ({
...state,
members: removeMember(state.members, action.payload),
}),
},
initialState
);
And if you then need the list as a string, in your component render() method - or preferrably in your react-redux mapStateToProps selector you can convert it to a string:
memberList = state.members.join(',')

Categories

Resources