Updating product quantity in Redux shopping cart - javascript

I'm asking for help as I'm new to react and javascript. There are similar solutions to my problem but I still can't get it working.
I'm trying to create a redux store with an option to be able to update quantity in the shopping cart.
This is my store
const cartSlice = createSlice({
name: "cart",
initialState: {
products: [],
quantity: 0,
},
reducers: {
addProduct: (state, { payload }) => {
const product = state.products.find(
(product) => product.id === payload.id
);
if (product) {
state = state.products.map((product) =>
product.id === payload.id
? {
...product,
quantity: (product.quantity += payload.quantity),
}
: product
);
} else {
state.products.push(payload);
state.quantity += 1;
}
},
incQuantity: (state, { payload }) => {
const product = state.products.find((product) => product.id === payload);
product.quantity++;
},
decQuantity: (state, { payload }) => {
const product = state.products.find((product) => product.id === payload);
if (product.quantity === 1) {
const index = state.products.findIndex(
(product) => product.id === payload
);
state.products.splice(index, 1);
} else {
product.quantity--;
}
},
removeProduct: (state, { payload }) => {
const index = state.products.findIndex(
(product) => product.id === payload
);
state.products.splice(index, 1);
},
},
});
export const { addProduct, incQuantity, decQuantity, removeProduct } =
cartSlice.actions;
export default cartSlice.reducer;
This is how I update quantity on the product page where you can add a product to the cart
const handleQuantity = (type) => {
if (type === "dec") {
quantity > 1 && setQuantity(quantity - 1);
} else {
setQuantity(quantity + 1);
}
};
<Remove onClick={() => handleQuantity("dec")} />
<span className="product-detail-amount">{quantity}</span>
<Add onClick={() => handleQuantity("inc")} />
<button className="product-detail-button"
onClick={() => dispatch(addProduct({ ...product, quantity }))}>
Add to Cart </button>
<Remove
onClick={() => dispatch(decQuantity(product.id))}/>
<span className="product-detail-amount">
{product.quantity}</span>
<Add
onClick={() => dispatch(incQuantity(product.id))}/>
What it does now it keeps adding quantity to the same product without displaying a new one, same issues with updating the quantity (it changes the quantity only for the first product and when it's gone it starts updating another one)
Your help is much appreciated!

I think the problem is in the incQuantity and decQuantity reducers where you comparing product id to whole payload.
Should't have been payload.id?
Like this
incQuantity: (state, { payload }) => {
const product = state.products.find((product) => product.id === payload.id);
product.quantity++;
},

I don't believe that product.quantity++ is updating the state, it's just updating the local variable inside the reducer.
I'm wing it pretty hard , but will this work?
incQuantity: (state, { payload }) => {
state.products[state.products.findIndex(el => el.id === payload)].quantity = payload;
},
Edit:
Got a little lost. I believe you want it to increment by 1: ++
state.products[state.products.findIndex(el => el.id === payload)].quantity++

I got what was the issue, the original code works, should've used _id instead of id (I'm pulling data from mongodb where id has an underscore, I think this caused the problem). Thanks everyone who replied!

Related

redux action to remove a list of items from the shopping cart or update the item quantity

I want to remove a list of items from the shopping cart or update the quantity if the item is available but not in the requested quantities. Does it make sense to use only a redux action to achieve my goal or it is better to have 2 different actions to avoid some business logic in the reducer? Here's my code:
case cartListItemRemoved: {
const productIds = action.payload;
const newFilteredItems = state.items.filter(
item => !productIds.find(el => el.id === item.id && el.stockAmount === 0),
);
const updatedFilteredItems = newFilteredItems.map(item => {
const { quantity } = productIds.find(el => el.id === item.id);
if (quantity && quantity > 0) {
return {
...item,
quantity: quantity,
};
}
return item;
});
return {
...state,
items: updatedFilteredItems,
};
}

Why do I get NaN value in react?

Whilst I am doing cart in react, I have no idea why I keep getting NaN value - only from a specific object data.
When I have the following data list:
#1 ItemsList.js
export const ItemsList = [
{
id: 1,
name: "VA-11 Hall-A: Cyberpunk Bartender Action",
price: 110000,
image: cover1,
link: "https://store.steampowered.com/app/447530/VA11_HallA_Cyberpunk_Bartender_Action/?l=koreana",
},
...
{
id: 6,
name: "Limbus Company",
price: 110000,
image: cover6,
link: "https://limbuscompany.com/",
},
];
And the following code, please look at the comment line.
#2 Goods.jsx
import React, { useContext } from "react";
import "./Goods.css";
import { DataContext } from "../../components/context/DataContext";
export const Goods = (props) => {
const { id, name, price, image, link } = props.shopItemProps;
const { cartItems, addItemToCart, removeItemFromCart } =
useContext(DataContext);
const cartItemStored = cartItems[id];
return (
<div className="goods">
<div className="goods-id">{id}</div>
<img src={image} alt="thumbnail_image" className="goods-image" />
<div className="goods-name">{name}</div>
<div className="goods-price">${price}</div>
<button>
<a href={link} className="goods-link">
Official Store Page
</a>
</button>
<div className="cart-button">
<button onClick={() => removeItemFromCart(id)}>-</button>
// ★Maybe here? but why do I get NaN only for id:6? Others work well.
{cartItemStored > -1 && <> ({cartItemStored}) </>}
<button onClick={() => addItemToCart(id)}>+</button>
</div>
</div>
);
};
What should I do to solve NaN? There seems to be no way to make that value as int in this case. Or do you see any problem from the above code block?
Edited
Sorry for confusing you. Here are the additional code related.
#3. DataContext.js (where cartItems state exists)
import React, { createContext, useState } from "react";
import { ItemsList } from "../ItemsList";
export const DataContext = createContext(null);
const getDefaultCart = () => {
let cart = {};
for (let i = 1; i < ItemsList.length; i++) {
cart[i] = 0;
}
return cart;
};
export const DataContextProvider = (props) => {
const [cartItems, setCartItems] = useState(getDefaultCart);
const checkoutTotalSum = () => {
let totalAmount = 0;
for (const item in cartItems) {
if (cartItems[item] > 0) {
let itemInfo = ItemsList.find((product) => product.id === Number(item));
totalAmount += cartItems[item] * itemInfo.price;
}
}
return totalAmount;
};
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
const removeItemFromCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] - 1 }));
};
const updateCartItemCount = (newAmount, itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: newAmount }));
};
const contextValue = {
cartItems,
addItemToCart,
removeItemFromCart,
updateCartItemCount,
checkoutTotalSum,
};
// console.log(cartItems);
return (
<DataContext.Provider value={contextValue}>
{props.children}
</DataContext.Provider>
);
};
The issue is in the function you are using to set the initial value of cartItems, more specifically, in the for loop. This line is the culprit: i < ItemsList.length, when in your case, it should be i <= ItemsList.length. Why? because you are not including the last element of ItemsList on the cart object (you are initializing the i counter with 1 and ItemsList's length is 6).
So, when you call addItemToCart:
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
And try to update the value corresponding to the last element of ItemsList which is 6 in cartItems, you're getting: '6': undefined + 1 because again, you did skip the last element in the for loop. This results in NaN.
You also have the option of initializing i with 0 and preserve this line: i < ItemsList.length, or:
for (let i = 1; i < ItemsList.length + 1; i++) {
...
}

Updating object in array hook react js

I trying to update the quantity by pressing the "up" button.
This is my code.
const increaseHandler = (x) => {
let newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist.quantity++;
} else {
exist = {
...props.cartItems,
quantity: 1,
};
}
newCart.push(exist);
props.setCartItems(newCart);
};
Here is the button:
<button className="up" onClick={() => increaseHandler(items)}>
up
</button>
Every time I click on the "up" button, it appends a duplicate of the item on the cartItems
I increased the quantity but it appends the object to the array.
You can update by using map:
const increaseHandler = (x) => {
props.setCartItems((preState) =>
preState.map((item) => (item.id === x.id ? { ...item, quantity: item.quantity + 1 } : item)),
);
};
Even though you've shallow copied the cart items array you are still mutating the individual items with the post increment.
const increaseHandler = (x) => {
let newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist.quantity++; // <-- mutation!!
} else {
exist = {
...props.cartItems,
quantity: 1,
};
}
newCart.push(exist); // <-- adds duplicate items
props.setCartItems(newCart);
};
You still need to create a new item reference, shallow copying the properties and updating the quantity property. You should only push new elements into the cart array.
const increaseHandler = (x) => {
const newCart = [...props.cartItems];
let exist = newCart.find((item) => item.id === x.id);
if (exist) {
exist = {
...exist,
quantity: exist.quantity + 1;
};
} else {
exist = {
...props.cartItems,
quantity: 1,
};
newCart.push(exist);
}
props.setCartItems(newCart);
};
It's more common to map the previous state to the next state, and I suggest using a functional state update to ensure correctly updating from the previous state versus any state closed over in callback scope (possibly stale).
const increaseHandler = (x) => {
props.setCartItems(items => {
const inCart = items.some((item) => item.id === x.id);
if (inCart) {
return items.map((item) => item.id === x.id
? {
...item,
quantity: item.quantity + 1,
}
: item);
}
return items.concat({
...props.cartItems,
quantity: 1,
});
});
};
You're setting the entire cartItems array into the single exist object you've created. So you're adding the entire cart to exist and then setting exist.quantity to be 1, and pushing that whole thing into newCart and setting it into state.
Try spreading exist rather than props.cartItems in your else block.
Based on your images and nomenclature, is this the FreeCodeCamp/Wiebenfalk tutorial on react & TS? If so, I did the same tutorial, and here's my add to cart function:
const handleAddToCart = (clickedItem: CartItemType) => {
setCartItems((prevState) => {
// check if item is in cart by looking for id of clicked item in array of cartItems already in state
const isItemInCart = prevState.find((item) => item.id === clickedItem.id);
if (isItemInCart) {
// search for item id matching clicked item; increment that amount
return prevState.map((item) =>
item.id === clickedItem.id
? { ...item, amount: item.amount + 1 }
: item
);
}
return [...prevState, { ...clickedItem, amount: 1 }];
});
};

component does not display updated redux state

I have a Meals page with meal cards that toggles a modal with details about the clicked meal when the card is clicked:
{
this.props.filteredMeals.map(m => {
return (
<div onClick={() => this.props.toggleMealModal(m)} key={m.id}>
<MealCard meal={m} />
</div>
)
})
}
toggleMealModal(meal) updates the meal inside my redux store.
meal: {
description: "Marre des sashimis de thon ou de saumon ? Tentez le maquereau!",
fees: "80",
id: 27,
ingredients: "maquereau cru",
name: "Sashimi de maquereau",
no_fees_price: 599,
original_price: 850,
}
inside my MealModal I have a PaymentContainer child component that also mounts when the modal is toggle and calls an action:
componentDidMount() {
this.props.getUserCredit(this.props.meal.no_fees_price, this.props.meal.fees);
}
where this.props.meal is fetched from my redux store via mapStateToProps.
the getUserCredit() is the following action:
export const getUserCredit = (noFeesPrice, fees) => {
return dispatch => {
axios.get('/get_user_credit')
.then(res => {
dispatch(setUserCredit(parseInt(res.data.credit) / 100));
parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit) < 0 ?
dispatch(setMealPrice(0, (parseInt(noFeesPrice) + parseInt(fees)) / 100))
:
dispatch(setMealPrice((parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit)) / 100, parseInt(res.data.credit) / 100)
);
})
.catch(err => console.log(err));
}
}
and then a child component displays the mealPrice, fetched from the redux store (and supposedly updated by getUserCredit()).
The problem is: the very first clicked meal card displays nothing at this.props.mealPrice, and when other cards are clicked and the modal mounts, this.props.mealPrice displays the price from the previously clicked meal card.
How can I change my code so that the right price mounts with the right meal ?
EDIT: relevant reducers and action creator code :
export const setUserCredit = (credit = 0) => ({
type: SET_USER_CREDIT,
payload: { credit }
});
export const setMealPrice = (mealPrice = null, amountOff = 0) => ({
type: SET_MEAL_PRICE,
payload: { mealPrice, amountOff }
});
case SET_USER_CREDIT: {
return {
...state,
credit: action.payload.credit
}
}
case SET_MEAL_PRICE: {
return {
...state,
amountOff: action.payload.amountOff,
mealPrice: action.payload.mealPrice
}
}
I recommend you to use status indicators while working with APIs.
Very simplified, you could build some action creators like:
const getUserCreditStart = () => ({
type: GET_USER_CREDIT_START,
});
const getUserCreditSuccess = () => ({
type: GET_USER_CREDIT_SUCCESS,
});
const getUserCreditError = payload => ({
type: GET_USER_CREDIT_ERROR,
payload,
});
In your getUserCredit function you can then dispatch these actions accordingly:
export const getUserCredit = (noFeesPrice, fees) => (dispatch) => {
dispatch(getUserCreditsStart());
return axios.get('/get_user_credit')
.then(res => {
dispatch(setUserCredit(parseInt(res.data.credit) / 100));
parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit) < 0
? dispatch(setMealPrice(0, (parseInt(noFeesPrice) + parseInt(fees)) / 100))
: dispatch(setMealPrice((parseInt(noFeesPrice) + parseInt(fees) - parseInt(res.data.credit)) / 100, parseInt(res.data.credit) / 100));
})
.then(() => dispatch(getUserCreditSuccess()))
.catch(err => dispatch(getUserCreditError(err)))
}
And then, you need to add them to a reducer. Btw. typeToReducer might be a good choice here. :)
In the reducer you need to set the status on the different actions that got dispatched.
const initialState = {
status: 'INITIAL',
};
...
case GET_USER_CREDIT_START: {
return {
...state,
status: 'START',
}
}
case GET_USER_CREDIT_SUCCESS: {
return {
...state,
status: 'SUCCESS',
}
}
case GET_USER_CREDIT_ERROR: {
return {
...state,
status: 'ERROR',
error: action.payload,
}
}
Well, and in your PaymentContainer component you then can wait for the answer and show a loading bar while you are waiting (the status information you get with mapStateToProps like you do with the results). In case of an error, you are able to display the error as well.

React-Redux Shopping Cart - Decrease/Increase Item Quantity

I am doing a project on the React-Redux shopping cart. I am currently trying to build out functionality to allow a user to update the quantity of items being added to the Shopping Cart. I've already been able to get "Remove from Cart" to work. I've been searching around for different ways of doing it, but it seems that all of the Shopping Cart tutorials stop at "Add to Cart"! So I've been trying to puzzle through it on my own, but found very few examples online. Can anyone point me in the right direction?
Here's the shopping cart tutorial originally posted on Github:
https://github.com/reactjs/redux/tree/master/examples/shopping-cart
Here's what I've been trying to figure out:
ProductItem.js
const ProductItem = ({product, onAddToCartClicked, onRemoveFromCartClicked, onIncreaseQuanityClicked, onDecreaseQuantityClicked }) => (
<div style={{ marginBottom: 20, marginLeft: 20}}>
<Card>
<CardBody>
<Product
title={product.title}
price={product.price}
inventory={product.inventory} />
<Button color="primary"
onClick={onAddToCartClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}>
{product.inventory > 0 ? 'Add to cart' : 'Sold Out'}
</Button>
<Button color="success"
onClick={onIncreaseQuanityClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}> +
</Button>
<Button color="danger"
onclick={onDecreaseQuantityClicked}
disabled={product.inventory > 0 ? '' : 'disabled'}> -
</Button>
<Button onClick={onRemoveFromCartClicked}>Remove</Button>
</CardBody>
</Card>
</div>
)
ProductsContainer.js
const ProductsContainer = ({ products, addToCart }) => (
<ProductsList title="Products">
{products.map(product =>
<ProductItem
key={product.id}
product={product}
onAddToCartClicked={() => addToCart(product.id)}
onIncreaseQuantityClicked={() => increaseQuantity(product.id)}
onDecreaseQuantityClicked={() => decreaseQuantity(product.id)} />
)}
</ProductsList>
)
ProductsContainer.propTypes = {
products: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
})).isRequired,
addToCart: PropTypes.func.isRequired,
increaseQuantity: PropTypes.func.isRequired
}
const mapStateToProps = state => ({
products: getVisibleProducts(state.products)
})
export default connect(
mapStateToProps,
{ addToCart, increaseQuantity, decreaseQuantity }
)(ProductsContainer)
reducer/products.js
const products = (state, action) => {
switch (action.type) {
case ADD_TO_CART:
return {
...state,
inventory: state.inventory - 1
}
case REMOVE_FROM_CART:
return {
...state,
inventory: state.inventory + 1
}
case INCREASE_QUANTITY:
return {
...state,
//NOT SURE WHAT ELSE TO PUT HERE
}
case DECREASE_QUANTITY:
return {
...state,
NOT SURE WHAT ELSE TO PUT HERE EITHER
}
default:
return state
}
}
Can anyone point me in the right path? If I'm even on the right path at all, or suggest any tutorials or websites that could help?
First, I think you should include quantity in your state and separate the logic of quantity from inventory. Here's what your state tree can look like:
{
cart: [
{id: 1, quantity: 3},
{id: 3, quantity: 2}
],
products: [
{id: 1, inventory: 10, ...},
{id: 2, inventory: 10, ...},
{id: 3, inventory: 10, ...}
]
}
Cart stores the products added to the cart and products contains all of the available products.
With this state tree in mind, we can use the following action creators:
function quantityUp(id, val){
return {type: 'QTY_UP', id, up: val}
}
function quantityDown(id, val){
return {type: 'QTY_DOWN', id, down: val}
}
Now, we can create our reducers. Since we separated quantity from inventory, we should also separate the reducers to reflect this logic.
const cart = (state, action) => {
switch(action.type){
case 'QTY_UP':
return Object.assign([], state.map(item => {
if(item.id === action.id){
item.quantity += action.up;
}
return item;
));
case 'QTY_DOWN':
return Object.assign([], state.map(item => {
if(item.id === action.id){
item.quantity -= action.down;
}
return item;
));
default:
return state;
}
};
The following actions should also be part of your cart reducer: ADD_TO_CART, REMOVE_FROM_CART
The products reducer should take care of modifying the products themselves, if needed. One case would be to modify the inventory of an item when an item has been purchased.
Let's create the action creators first:
//cart will be an array
function purchase(cart){
return {type: 'PURCHASE', cart}
}
Now we can create the reducer:
const products = (state, action) => {
switch(action.type){
case 'PURCHASE':
const ids = action.cart.map(item => item.id);
return Object.assign([], state.map(item => {
if(ids.includes(item.id)){
item.inventory -= action.cart.filter(p => p.id === item.id)[0].quantity;
}
return item;
}));
case default:
return state;
}
};
Now we can add products to your cart, edit the quantities of each product in the cart, and update the inventory of each product in your state when a product has been purchased.
Be sure to have an initial state setup before your reducer like:
const initialState = {
inventory: 0,
quantity: 0
}
Then you link your reducer to the state that you just declared:
const products = (state = initialState, action) => {
if you want your action to increase your quantity in state, you proceed as with inventory:
quantity: state.quantity + 1
As a reminder, the state is first initiated through one or multiple reducers and you create a store in redux by using for example
const store = createStore(yourReducer)
or
const store = createStore(combineReducers(allYourReducers))
Your store will have the global state of your app made of the sum of all your reducer's initialStates.
Then you can access and play with the state by dispatching your actions
store.dispatch(yourAction)
If everything in your app is well connected, you should see your state updating as you want.
You may check this course from Andrew Mead : https://www.udemy.com/react-2nd-edition/learn/v4/overview
Given your current code, addToCart and increaseQuantity are the same thing.
You can either:
1) Reuse the addToCart function in your container
<ProductItem
key={product.id}
product={product}
onAddToCartClicked={() => addToCart(product.id)}
onIncreaseQuantityClicked={() => addToCart(product.id)}
onDecreaseQuantityClicked={() => decreaseQuantity(product.id)} />
2) Implement the same logic in your reducer
case INCREASE_QUANTITY:
return {
...state,
inventory: state.inventory - 1
}
when we work on shopping cart, we should have cartItems array inside our cart state and every time we click on "add to cart" button, that item will be pushed to that array and we will "map" that array in the component we wanna render cart items.
const INITIAL_STATE = {
//you could have more properties but i focus on cartItems
cartItems: []
};
to add item to the cart, we should be careful when we write our code. because first time adding an item to the cart is easy, but what if we add the same item multiple times to the cart. so we need to group items inside the cartItems array. for this we need to write an utility function.
//cart.utils.js
export const addItemToCart = (cartItems, cartItemToAdd) => {
//find(condition) finds the first item in the array based on the condition.
const existingCartItem = cartItems.find(item => item.id === cartItemToAdd.id);
if (existingCartItem) {
//in order for change detection to trigger we have to rerender
//otherwise our quantity property will not be updated
//map will return a new array
//we need to return new versions of our state so that our component know to re render
//here we update the quantity property
return cartItems.map(item =>
item.id === cartItemToAdd.id
? { ...cartItemToAdd, quantity: item.quantity + 1 }
: item
);
}
//when you first time add a new item, sine exixtingCartItem will be falsy, it will pass the first if block and will come here
//quantity property gets attached the first time around since this if block wont run when it is a new item.
//in the beginning cartItems array is empty. every time you add a new item to this array, it will add "quantity:1" to this item object.
return [...cartItems, { ...cartItemToAdd, quantity: 1 }];
};
in your reducer file
import { addItemToCart } from "./cart.utils";
case INCREASE_QUANTITY:
return {
...state,
cartItems: addItemToCart(state.cartItems, action.payload)
};
for removing item from the cart, we need to write another utility function.
cart.utils.js
export const removeItemFromCart = (cartItems, cartItemToRemove) => {
//check if item is already in the cartItems
const existingCartItem = cartItems.find(
item => item.id === cartItemToRemove.id
);
//if there is only 1, upon clicking, we should remove the item from the array
if (existingCartItem.quantity === 1) {
return cartItems.filter(item => item.id !== cartItemToRemove.id);
}
return cartItems.map(item =>
item.id === cartItemToRemove.id
? { ...item, quantity: item.quantity - 1 }
: item
);
};
in reducer/products.js
import { addItemToCart, removeItemFromCart } from "./cart.utils";
case DECREASE_QUANTITY:
return {
...state,
cartItems: removeItemFromCart(state.cartItems, action.payload)
}

Categories

Resources