so my reducer looks like that.
I want to add items to cart and if the item is already in the state it should increase the qty of that item.
I pass object to payload but it doesnt contain the quantity field so i create it manually.
But when i add more than 2 of the same item it doesnt update further.
What is the issue here?
const init = { cartItems: [] };
export const cartReducer = (state = init, action) => {
switch (action.type) {
case 'ADD_TO_CART': {
const theItem = state.cartItems.find(
(item) => item.title === action.payload.title
);
if (theItem) {
theItem.quantity++;
return { ...state };
} else {
const updatedCart = [...state.cartItems, action.payload];
return { ...state, cartItems: updatedCart };
}
}
default: {
return { ...state };
}
}
};
You cannot mutate the state, it will then not update the react component, you can try the below code for updating the quantity
const init = { cartItems: [] };
export const cartReducer = (state = init, action) => {
switch (action.type) {
case 'ADD_TO_CART': {
const itemIndex = state.cartItems.findIndex(
(item) => item.title === action.payload.title
);
if (itemIndex !== -1) {
const newCartItems = [...state.cartItems];
const newItem = {...newCartItems[itemIndex]}
newItem.quantity += 1;
newCartItems[itemIndex] = newItem;
return { ...state, cartItems: newCartItems};
} else {
const updatedCart = [...state.cartItems, action.payload];
return { ...state, cartItems: updatedCart };
}
}
default: {
return { ...state };
}
}
};
EDIT: The other option you can use is with Immer js it makes state management easier, though it will add in bundle size, if you have strict budget of bundle size then you would need to think of adding Immer js, Below how you can write the logic with Immer js.
import produce from 'immer'
const init = { cartItems: [] };
export const cartReducer = (state = init, action) =>
produce(state, draft => {
switch (action.type) {
case 'ADD_TO_CART': {
const itemIndex = draft.cartItems.findIndex(
(item) => item.title === action.payload.title
);
if (itemIndex !== -1) {
draft.cartItems[itemIndex].quantity += 1;
} else {
draft.cartItems.push(action.payload)
}
break;
}
default: {
break;
}
}
})
Give it a try, Immer js Documentation, Redux and Immer
Related
Error type: Store does not have a valid reducer. Make sure the
argument passed to combineReducers is an object whose values are
reducers.
I am trying to add a Store and my app is failing. I don't understand where the error is coming from. Can someone point out my error and how to fix it?
store
import { configureStore } from '#reduxjs/toolkit';
import rootReducers from './reducers';
const store = configureStore({
reducer: rootReducers,
});
export default store;
reducer
import handleCart from './handleCart';
import { combineReducers } from 'redux';
const rootReducers = combineReducers({
handleCart,
});
export default rootReducers;
const cart = [];
const handleCart = (state = cart, action) => {
const product = action.payload;
switch (action.type) {
case ADDITEM:
// Check product exist
const exist = state.find(x => x.id === product.id);
if (exist) {
return state.map(x =>
x.id === product.id ? {...x, qty: x.qty + 1} : x,
);
} else {
const product = action.payload;
return [
...state,
{
...product,
qty: 1,
},
];
}
break;
case REMOVEITEM:
const exist1 = state.find(x => x.id === product.id);
if (exist1.qty === 1) {
return state.filter(x => x.id !== exist1.id);
} else {
return state.map(x =>
x.id === product.id ? {...x, qty: x.qty - 1} : x,
);
}
break;
default:
break;
}
};
I see only a couple overt issues in the handleCart reducer function code, either could be cause for throwing an exception. It seems the handleCart reducer function is missing returning state in the default case. The REMOVEITEM case also incorrectly assumes exist is defined and accesses a qty property of a potentially undefined object. Remember that Array.prototype.find returns undefined when no matching element is found in the array.
Make sure the handleCart is default exported so it can be imported to create the root reducer.
const cart = [];
const handleCart = (state = cart, action) => {
const product = action.payload;
switch (action.type) {
case ADDITEM: {
// Check product exist
const exist = state.find(x => x.id === product.id);
if (exist) {
return state.map(x =>
x.id === product.id ? { ...x, qty: x.qty + 1 } : x,
);
}
return [...state, { ...product, qty: 1 }];
}
case REMOVEITEM: {
const exist = state.find(x => x.id === product.id);
if (exist) {
// item exists, update quantity
if (exist.qty === 1) {
return state.filter(x => x.id !== exist1.id);
} else {
return state.map(x =>
x.id === product.id ? { ...x, qty: x.qty - 1 } : x,
);
}
}
// item didn't exist, just return current state
return state;
}
default:
// No action to do, return current state
return state;
}
};
export default handleCart;
You are using redux-toolkit, why are you not creating state slice? Here's a similar implementation using createSlice. This allows you to write the reducer functions using mutable updates instead of needing to shallow copy all the parts of the state that are being updated.
import { createSlice } from '#reduxjs/toolkit';
const initialState = [];
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addItem: (state, action) => {
const product = state.find(product => product.id === action.payload.id);
if (product) {
product.qty++;
} else {
state.push({ ...action.payload, qty: 1 });
}
},
removeItem: (state, action) => {
const product = state.find(product => product.id === action.payload.id);
if (product) {
product.qty--;
if (product.qty === 0) {
state.filter(product => product.id !== action.payload.id);
}
}
},
},
});
export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
export const initialState = {
ids: [],
basket: [],
subtotal: 0
}
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_BASKET':
debugger
if (state.ids.indexOf(action.item.id) !== -1) {
// if not new item increase a quantity
console.log(state.basket.find((el) => el.id.match(action.item.id)).quantity);
++state.basket.find((el) => el.id.match(action.item.id)).quantity;
state.subtotal += action.amount
}
if (state.ids.indexOf(action.item.id) === -1) {
//if new value push id to array and push new value to basket
state.ids.push(action.item.id)
state.subtotal += action.amount
state.basket = [...state.basket, action.item]
}
return {
...state,
}
default:
return state;
}
}
I've used the debugger and where time to increase quantity it worked twice,
if I change return { ...state } to return state all works right only one-time increase, but UI isn't refreshing (basket counter)
I need to increase property in the state, but I have no idea how I can do it
help please, I have tried two days but got many troubles
export const initialState = {
ids: [],
basket: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TO_BASKET':
if (state.ids.indexOf(action.item.id) === -1) {
// if new item add to basket
return {
ids: [...state.ids, action.item.id],
basket: [...state.basket, action.item]
}
}
else {
if (state.basket.length > 0) {
// state.basket[state.basket.length - 1].quantity += 1
}
return {
...state,
// IT'S WRONG
basket: [...state.basket, [state.basket.length - 1].quantity += 1]
}
}
default:
return state;
}
}
You mutate state (basket.quantity += 1) when it should treated as immutable.
I believe you need to rethink how you structure your state if it gets so hard to immutably update a state.
export const initialState = {
ids: [],
basket: [],
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TO_BASKET":
if (state.ids.indexOf(action.item.id) === -1) {
...
} else {
...
const basketsShallowCopy = [...state.basket];
const basketToUpdate = basketsShallowCopy.pop();
const updatedBasked = {
...baskedToUpdate,
quantity: basketToUpdate.quantity + 1,
};
basketsShallowCopy.push(updatedBasked);
return {
...state,
basket: basketsShallowCopy,
};
}
...
}
};
I know that redux trigger react component to re-render once state of that component get changed but this doesn't happen in my situation.
Action
const addToCart = (product, cartProducts) => {
let newCartProducts = [...cartProducts, product];
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeated = acc.find(p => p.id === cur.id);
if(repeated) repeated.quantity++;
else acc.push(cur);
return acc;
}, []);
return {
type: ADD_TO_CART,
payload: newCartProducts
}
}
Reducer:
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
return action.payload;
default:
return state;
}
}
The reducer returns a new state every time the action dispatched from the component but i need to close the cart and open it again to get the effect, redux does not update the product quantity simultaneously??
You are modifying the existing elements in the state.
Use
newCartProducts = newCartProducts.reduce((acc, cur) => {
let repeatedIndex = acc.findIndex(p => p.id === cur.id);
const repeated = acc[repeatedIndex];
if (repeated) {
acc[repeatedIndex] = {
...repeated,
quantity: repeated.quantity + 1
};
} else acc.push(cur);
return acc;
}, []);
You array is recreated each time, but the objects inside it are not. So when you modify their internals you need to notify that the specific object has changed.
Refactor logic to the reducer and set the quantity here:
const addToCart = product => {
return {
type: ADD_TO_CART,
payload: product,
};
};
//I assume state is an array of products on your cart
export default (state = [], action) => {
switch (action.type) {
case ADD_TO_CART:
const { id } = action.payload;
return state.map(p => p.id).includes(id)
? //product is already on card add quanity
state.map(p =>
p.id === id
? { ...p, quantity: p.quantity + 1 }
: p
)
: state.concat({ ...action.payload, quantity: 1 }); // add product
default:
return state;
}
};
I need to toggle somehove adding/removing object from redux store. It is like check/uncheck. I have following code:
const list = ({ id, item }) => {
const isChecked = name => items.some(item => item.name === name);
let itemClass = cx({
item: true,
selected: isChecked(name),
});
return (
<li className={itemClass}
onClick={() => click(fullItem)} key={id}>
<div className={styles.name}>
{isChecked(name) ?
(<span><i className={`fa fa-check`}></i>{name}</span>)
: (<span>{name}</span>)
}
</div>
</li>
);
}
export const click = item => ({
type: ADD_ITEM,
payload: item,
});
import {
ADD_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ITEM:
return {
...state,
items: [action.payload],
};
default:
return state;
}
};
but for now it only work for adding item to store, when I click on item when it is selected, it should remove it from the store. How can I toggle onclick removing/adding object to redux store?
You could try something like this. Changing the ADD_ITEM instead to a TOGGLE_ITEM where you check for existence of the item using something like Array.prototype.find. Adding if it does not exist, and removing it if it does exist:
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
const currentItem = state.items.find(item => item.id === action.payload.id);
if (!currentItem) {
return {
...state,
items: [...state.items, action.payload],
};
} else {
const newItems = state.items.filter(item => item.id !== action.payload.id];
return {
...state,
items: [...newItems]
};
}
default:
return state;
}
};
You may though want to consider having separate add, update, and delete actions, and dispatch the different actions accordingly from your components.
export const click = item => ({
type: TOGGLE_ITEM,
payload: item,
});
import {
TOGGLE_ITEM,
} from "../actions";
const initialState = {
items: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_ITEM:
// check to see if the item already in our array
// Array.some return true/false
const itemAlreadyExists = state.items.some(item => item.id === action.payload.id)
return {
...state,
// if the item already in our array filter it
// if not just add it
items: itemAlreadyExists
? state.items.filter(item => item.id !== action.payload.id)
: [...state.items, action.payload],
};
default:
return state;
}
};