How to prevent my items from duplicating using Redux Toolkit + Strapi + React? - reactjs

My Items keep on duplicating on shopping cart and I cant seems to figure out. Hope to get some guidance here and would be a great help for my project!
Below are my code :
CartReducer.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
isCartOpen: false,
cart: [],
count: [],
products: [],
};
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
setProducts: (state, action) => {
state.products = action.payload;
},
addToCart: (state, action) => {
const item = state.cart.find(
(product) => product.id === action.payload.id
);
console.log(item);
if (item) {
item.count += action.payload.count;
} else {
return {
...state,
cart: [...state.cart, action.payload.product],
};
}
},
Full Github repo : https://github.com/TheKelvinT/Teck-Hong-CS.git
I've tried looking up several sample projects and changing my code based on those but it still does not work.

the problem you have there, that:
item.count += action.payload.count;
this line actually does nothing, because you don't use this down stream, to make it work that way you would need something like that:
const itemIndex = state.cart.findIndex((product) => product.id === action.payload.id);
if (itemIndex !== -1) {
state.cart[itemIndex] = {...state.cart[itemIndex], count: action.payload.count }
} else ...
Just a note that it is not obvious where you keep your count, the line above adds count to item in the cart.
There might be more issues with this code, hover i can recommend in terms of cart use state normalization technique reference. Although that it may look a bit strange, it is quite easy and powerful:
The first thing is to remove products from cart slice, with slices, it's really better to have them as flat as possible, it's much easier to extend after:
/slices/products.js
import { createSlice } from "#reduxjs/toolkit"
const { actions, reducer } = createSlice({
name: 'products',
initialState: [],
reducers: {
setProducts: (state, {payload}) => payload,
}
})
export const productsReducer = reducer;
export const { setProducts } = actions;
Cart slice, is going to have to main properties, the first one is ids an array of id's of products added to cart, the second one would be products it's so called key value pair with key is product id, value is an object with product property and count an integer for count:
/slices/cart.js
import { createSelector, createSlice } from '#reduxjs/toolkit';
const { actions, reducer } = createSlice({
name: 'cart',
initialState: {
ids: [],
products: {},
},
reducers: {
// the payload is { product, count }
addCart: (state, { payload }) => {
if (!state.ids.includes(payload.product.id)) {
state.ids.push(payload.product.id);
state.products[payload.product.id] = { product: payload.product, count: payload.count };
} else state.products[payload.product.id].count += payload.count;
// you should always validate the count e.g. Math.max(0, payload.count)
},
// this would just remove from cart, you can do yourself count change
removeCart: (state, { payload }) => {
if (state.ids.includes(payload.product.id)) {
state.ids = state.ids.filter((id) => id !== payload.product.id);
delete state.products[payload.product.id];
}
},
},
});
export const cartReducer = reducer;
export const { addCart, removeCart } = actions;
// BONUS, is selector that checks if object is in the cart
// const isInCart = useSelector((state) => isInCartSelector(state, id))
export const isInCartSelector = createSelector(
(state, id) => state.cart,
(state, id) => id,
(cart, id) => cart.ids.includes(id)
);

Related

Error: [Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft. redux toolkit

I am making this shopping cart in redux-toolkit and rtk query. I want to change the cartItem index with product id.
I think the problem is with this line of code:
const cartItems = state.cartItems
My full code:
import { createSlice, current } from '#reduxjs/toolkit';
const initialState = {
cartItems: [],
};
export const cartSlice = createSlice({
name: 'cartSlice',
initialState: initialState,
reducers: {
setToCart: (state, action) => {
const { payload:product, newQty = 1 } = action;
const cartItems = state.cartItems;
const quantity = cartItems?.[product?._id]
? parseInt(cartItems[product._id].quantity + newQty)
: 1;
cartItems[product._id] = {
...product,
quantity,
};
return {
...state,
cartItems: cartItems
}
},
},
});
export const {setToCart} = cartSlice.actions;
export default cartSlice.reducer;
Here is action.payload:
{
img: "_OCT2HGtyHbioAuVTMGcA-mauntain.jpg",
name: "iphone",
price: 600001,
_id: "60d375ed3224711bc0f3538a"*
}
As the error states, when using an Immer-powered reducer you must Either return a new value or modify the draft.
You are modifying the cartItems array. You do not need to return anything from your reducer. Just modify the values that you want to change. That's the magic of Redux Toolkit! (powered by Immer).
There are some other issues with your reducer:
action will not have a property newQty. It will only have a type and payload. Perhaps newQty is a property of the payload?
It seems like cartItems is a dictionary keyed by _id, but you initialize it as an empty array [] instead of an empty object {}.
parseInt doesn't make sense outside of an addition statement. parseInt("2" + "2") will be 22, not 4.
A "fixed" version might look like this, depending on how you structure your payload:
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
cartItems: {},
};
export const cartSlice = createSlice({
name: 'cartSlice',
initialState: initialState,
reducers: {
setToCart: (state, action) => {
const { payload } = action;
const { product, newQty = 1 } = payload;
const cartItems = state.cartItems;
// If already in cart, increase quantity.
if (cartItems[product._id]) {
cartItems[product._id].quantity += newQty;
}
// Otherwise add to cart.
else {
cartItems[product._id] = {
...product,
quantity: newQty
}
}
// Don't return anything.
},
},
});
export const {setToCart} = cartSlice.actions;
export default cartSlice.reducer;

Redux Toolkit remove an item from array (Everything is being deleted)

I am trying to make a system like shopping cart with redux and react. The products are stored in a redux slice array as a whole object. The product object goes like this:
This is my code for my checkbox input
const products = useSelector((state) => state.prodSlice.value)
const handleChange = (event) => {
const button = event.target
const isActive = button.checked
const itemName = event.currentTarget.id
const items = products.items
const itemsArr = {}
items.forEach((items) => {
if (items.productName === itemName) {
itemsArr['productName'] = items.productName
itemsArr['total'] = items.total
itemsArr['quantity'] = items.quantity
if (isActive) {
dispatch(checkout({products: itemsArr}))
} else {
dispatch(removeItem({products: itemsArr}))
}
}
})
}
When adding products to the array, there is no problem,
However, when I uncheck an item, and get the value of array it returns just an empty array instead of removing just 1 item.
I just want to delete that one item from the array, here is my redux slice code
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
value: {
products: [],
}
}
export const checkOut = createSlice({
name: "checkout",
initialState,
reducers: {
checkout: (state, action) => {
state.value.products.push(action.payload)
},
removeItem: (state, action) => {
state.value.products = state.value.products.filter((products) => products.produdctName !== action.payload.productName)
}
}
})
export const { checkout, removeItem } = checkOut.actions
export default checkOut.reducer
I hope someone can help me pls
your removeItem reducer should simply return the filtered array
removeItem: (state, action) => {
return state.value.products.filter((products) => products.productName !== action.payload.productName)
}

Best way to share state between slices in redux toolkit?

If I have a slice like this
cartSlice.js
import { createSlice } from "#reduxjs/toolkit";
import cartItems from "../cartItems";
const initialState = {
cartItems: cartItems,
amount: 1,
total: 0,
isLoading: true,
};
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
clearCart: (store) => {
store.cartItems = [];
},
removeItem: (store, action) => {
store.cartItems = store.cartItems.filter(
(item) => item.id !== action.payload
);
},
increase: (store, action) => {
const target_item = store.cartItems.find(
(item) => item.id === action.payload.id
);
target_item.amount++;
},
decrease: (store, { payload }) => {
const target_item = store.cartItems.find(
(item) => item.id === payload.id
);
target_item.amount--;
},
calculate: (store) => {
let amount = 0;
let total = 0;
store.cartItems.forEach((item) => {
amount += item.amount;
total += item.amount * item.price;
});
store.amount = amount;
store.total = total;
},
},
});
export const { clearCart, removeItem, increase, decrease, calculate } =
cartSlice.actions;
export default cartSlice.reducer;
How do I implement something like this?
checkoutSlice.js
const initialState = {
purchasedItems: [],
checkoutIsOpen: false,
};
const { cartItems, amount } = useSelector((store) => store.cart); <--HYPOTHETICALLY
const checkoutSlice = createSlice({
name: "checkout",
initialState,
reducers: {
addToCheckout: (state) => {
if (amount >= 1) { <------HERE
state.purchasedItems.push(cartItems); <---HERE
}
},
openCheckout: (state) => {
state.checkoutIsOpen = true;
},
},
});
export const { addToCheckout, openCheckout } = checkoutSlice.actions;
export default checkoutSlice.reducer;
You can't use selectors, what else is left? I've read a ton of previous posts that say it's not possible, but then how do you create functional websites with components that interact with each other? Like in this case with basic shopping app with a checkout cart, how do you get the selected items into the checkout cart? It's just not possible? That doesn't make sense because isn't that basic core functionality of a website? Why wouldn't redux allow this? I feel like there has to be a way.
I think I'm fundamentally misunderstanding here.
Any help? How do I accomplish this?

issues on redux toolkit EntitiyAdapter

I created a website and use redux-toolkit
but I have an issue with createSlice. my two records data receive correctly. but when I set data into adapter just the first record was added. this is my slice code
import { createEntityAdapter, createSlice, PayloadAction, createSelector, createAsyncThunk } from "#reduxjs/toolkit";
import { fetchTop10Podcasts } from "../../../services/endpoints/Podcasts";
import { podcastState } from "../../initialStates/Podcasts";
const podcastAdapter = createEntityAdapter();
// interface podcastState {
// entities: Array<Podcast>
// loading: 'idle' | 'loading' | 'successed' | 'failed'
// }
export const PodcastSlice = createSlice({
name: 'podcasts',
initialState: podcastAdapter.getInitialState({
entities: [],
loading: 'idle'
} as podcastState),
reducers: {
}, extraReducers: (builder) => {
builder.addCase(fetchTop10Podcasts.pending, (state, action) => {
state.loading = 'loading';
}).addCase(fetchTop10Podcasts.fulfilled, (state, action) => {
state.loading = 'successed';
podcastAdapter.setAll(state, action.payload.data.podcasts)
console.log('podcasts', action.payload.data.podcasts); // here I recieved two record
}).addCase(fetchTop10Podcasts.rejected, (state, action) => {
state.loading = 'failed';
})
}
});
export const {
selectById: selectPodcastById,
selectAll: selectPodcasts
} = podcastAdapter.getSelectors((state: any) => state.podcasts)
export const selectPodcastIds = createSelector(
selectPodcasts,
podcasts => podcasts.map((podcast: any) => podcast._id)
)
export default PodcastSlice.reducer;
I think the issue may be with your initial state.
When you call adapter.getInitialState(), the state structure it returns is {ids: [], entities: {}}, plus whatever additional fields you passed in.
You're providing entities: [] as an additional field. It's likely you're overwriting the original object structure as a result.
Remove that line and I think this will work.

cannot update initalState in redux-toolkit with createAsyncThunk

Im trying to set and update initialState in redux toolkit after fetch operation
pageSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { deletePage, getPages, savePage, updatePage } from "../../services/page.service";
import { initialPage } from "../../components";
export const getUserPages = createAsyncThunk(
"pages/getUserPages",
async() => {
const pages = await getPages();
return pages
}
)
export const saveUserPage = createAsyncThunk(
"pages/saveUserPage",
async(page) => {
const savedPage = await savePage(page);
return savedPage;
}
)
export const pageSlice = createSlice({
name: "pages",
initialState: {
pageStatus: "idle",
pages: []
},
reducers: {},
extraReducers: {
[getUserPages.pending]: (state) => {
state.pageStatus = "loading";
},
[getUserPages.fulfilled]: (state, action) => {
state.pages = action.payload
state.pageStatus = "pageLoaded"
},
[getUserPages.rejected]: (state) => {
state.pageStatus = "error"
},
[saveUserPage.pending]: (state) => {
state.pageStatus = "loading";
},
[saveUserPage.fulfilled]: (state, action) => {
state.pages.push(action.payload)
state.pageStatus = "pageLoaded"
},
[saveUserPage.rejected]: (state) => {
state.pageStatus = "error"
}
}
})
export default pageSlice.reducer;
initialState: {
pageStatus: "idle",
pages: []
},
Working on note app with redux-toolkit. pages array will contain array of objects.
In extraReducers
[saveUserPage.fulfilled]: (state, action) => {
// console.log(state.pages) if empty( undefined ) else [{element}]
state.pages.push(action.payload)
state.pageStatus = "pageLoaded"
}
if initailState pages array contain any single element [saveUserPage.fulfilled] work fine.
but if array is empty then i get error
Cannot read property 'push' of undefined at pages/saveUserPage/fulfilled
if console.log(state.pages) in s
What I'm doing wrong ?
Based on the Error message you have mentioned Cannot read property 'push' of undefined at pages/saveUserPage/fulfilled, state.pages is not an empty array, It is undefined. That is the reason you are seeing this error.
Array.push operation, all it expects the variable to be array i.e. [], It doesn't matter whether it is empty or it has items.
Please check the below code block or some other operation, whether it is assigning it as 'undefined' in the first place.
[getUserPages.fulfilled]: (state, action) => {
state.pages = action.payload
state.pageStatus = "pageLoaded"
},
Workaround solution:
[saveUserPage.fulfilled]: (state, action) => {
if(state.pages === undefined){
state.pages = [];
if(action.payload && action.payload.length > 0){ / Make sure that payload is an array
state.pages = action.payload; //
}
}
else{
state.pages.push(action.payload);
}
state.pageStatus = "pageLoaded"
},

Resources