Reputation: 10653
I've searched for solutions on Google and SO but still cannot find an answer. They all stop at "Add item to cart" or "increase/decrease quantity" but never on calculating the Total, which is annoying!
In my app, there's a list of items where the user can enter the quantity if an item, which updates the its price. All I want to know is how do you sum up all the prices of all items in a cart into a Total price, with Redux and display it in my React app?
Also, if you can point me to any good shopping cart tutorial that actually goes beyond listing products in a cart, I'll be glad.
/**
* @description UPDATE SINGLE ITEM PRICE WHEN USER ENTER QUANTITY
*
* @param{number} price
* @param{number} quantity - enter by user from input
*/
export const updateItemPrice = (price, quantity) => dispatch => {
const result = price * quantity;
return dispatch({
type: UPDATE_ITEM_PRICE,
subtotal: result
});
};
const INITIAL_STATE = {
total: 0,
subtotal: 0
};
const productsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
// Update single item price
case Types.UPDATE_ITEM_PRICE:
return {
...state,
subtotal: action.subtotal,
total: // --> Here is where I'm stuck!!
};
default:
return state;
}
};
Upvotes: 4
Views: 16066
Reputation: 49729
You can use native javascript reduce(). you will reduce the cartItems into total price with quantity and price properties of the product object.
if you are working on a shopping cart, you should have cart reducer and cart state which has the "cartItems" array property. another thing is each product object should have quantity and price object.
const INITIAL_STATE = {
//you might have more properties
cartItems: []
};
so in your component(usually checkout component) that you wanna display total price write this:
const mapStateToProps = state => {
total: state.cart.cartItems.reduce(
//reduce go through the array and cartItem is the each item in the array
(accumulatedTotal, cartItem) =>
accumulatedTotal + cartItem.price * cartItem.quantity,
0 //0 is the start point of accumulatedTotal
);
};
and then you can pass "total" to your component as prop
this is an example of a checkout page
const CheckoutPage = ({ cartItems, total }) => (
<div className="checkout-page">
<div className="checkout-header">
<div className="header-block">
<span>Product</span>
</div>
<div className="header-block">
<span>Description</span>
</div>
<div className="header-block">
<span>Quantity</span>
</div>
<div className="header-block">
<span>Quantity</span>
</div>
<div className="header-block">
<span>Remove</span>
</div>
</div>
{cartItems.map(cartItem => (
<CheckoutItem key={cartItem.id} cartItem={cartItem} />
))}
<div className="total">
<span>TOTAL:${total}</span>
</div>
</div>
);
Upvotes: 1
Reputation: 4344
EDIT: More complete example of state/actions/reducers.
Do you actually need to store the totals in redux? Generally you want to keep the minimal state in redux, and calculate any derived data that you can in a selector. Subtotals and totals definitely fall into this category (unless you have a really unusual set your own price set up or something), so instead of storing them in the store, you can calculate them as needed, for example as part of your mapStateToProps
function (assuming you're using react-redux
).
Here's an example of what your state could look like. It includes two main slices, one for the catalog of items, and a second one specifically for the card.
{
itemDetails: {
item01: { name: 'Item One', price: 9.95 },
item02: { name: 'Item Two', price: 10 },
item03: { name: 'Item not appearing in this example', price: 50 },
},
cart: {
item01: 1,
item02: 2,
},
}
Looking at the cart slice, all the reducer for that needs to do is manage the quantity in the cart, which you can do with basic actions. The actions and reducer may look something like (none of this is tested, is just to provide a feel for how this may look):
// Cart actions
const incrementQuantityInCart = (itemId) => ({
type: 'incrementQuantityInCart',
itemId,
})
const decrementQuantityInCart = (itemId) => ({
type: 'decrementQuantityInCart',
itemId,
})
const removeItemFromCart = (itemId) => ({
type: 'removeItemFromCart',
itemId,
})
// Cart reducer, would be combined with a separate reducer using redux's `combineReducers`
const cart = (state = {}, action) => {
switch (action.type) {
case 'incrementQuantityInCart':
const currentQuantity = state[action.itemId] || 0
return {
...state,
[action.itemId]: currentQuantity + 1,
}
case 'decrementQuantityInCart':
const currentQuantity = state[action.itemId] || 0
return {
...state,
[action.itemId]: Math.max(currentQuantity - 1, 0),
}
case 'removeItemFromCart':
return {
...state,
[action.itemId]: 0,
}
default:return state
}
}
You could then have a selector such as:
function getCartContents(state) {
const itemsInCart = Object.keys(state.cart)
.filter(itemId => state.cart[itemId] > 0)
.map(itemId => {
const quantity = state.cart[itemId]
const itemDetail = state.itemDetails[itemId]
return {
name: itemDetail.name,
price: itemDetail.price,
quantity,
subtotal: itemDetail.price * quantity,
}
})
const total = itemsInCart.reduce((total, item) => total + item.subtotal)
return { itemsInCart, total }
}
// example output based on the above state
// {
// itemsInCart: [
// {
// name: 'Item One',
// price: 9.95,
// quantity: 1,
// subtotal: 9.95,
// },
// {
// name: 'Item Two',
// price: 10,
// quantity: 2,
// subtotal: 20,
// },
// ],
// total: 29.95,
// }
You can then use this function either in or as your mapStateToProps
for whatever component you want and it will have access to this data in it's props, so you can use as required.
Upvotes: 7
Reputation: 141
It looks like you do it in a really wrong way. You should keep a list of cart items in the state. In this case you will be able to calculate cart total any time you need it, in any place, not necessary in reducer. The code should look like this
export const addItem = (item/*{id, price}*/, quantity) => dispatch => {
return dispatch({
type: ADD_ITEM,
item,
quantity
});
};
export const removeItem = (item/*{id, price}*/, quantity) => dispatch => {
return dispatch({
type: REMOVE_ITEM,
item,
quantity
});
};
const INITIAL_STATE = {
items: {},
total: 0
};
const cartReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
// Update single item price
case Types.ADD_ITEM:
{
const items = Object.extend(state.items);
const { item, quantity } = action;
if (items[item.id]) {
items[item.id].quantity += quantity;
} else {
items[item.id] = {price: item.price, quantity};
}
const total = Object.values(items)
.reduce((result, cartItem) => result + cartItem.price*cartItem.quantity, 0);
return {
...state,
items,
total
};
}
case Types.REMOVE_ITEM:
{
const items = Object.extend(state.items);
const { item, quantity } = action;
if (items[item.id]) {
items[item.id].quantity -= quantity;
} else {
items[item.id] = {price: item.price, quantity};
}
if (items[item.id] <= 0) {
delete items[item.id];
}
const total = Object.values(items)
.reduce((result, cartItem) => result + cartItem.price*cartItem.quantity, 0);
return {
...state,
items,
total
};
}
default:
return state;
}
};
The code is just to demonstrate a general idea, reducers code is a copy/paste and common parts can be extracted.
Upvotes: 0
Reputation: 56
I think you need to change the way you structured the cart object stored in the cart. It should be something like this
cart: [
{
key: /* UNIQUE KEY TO IDENTIFY ITEM IN*/
price: /* Number */
quantity: /* number */
total: /*price*quantity*/
},
{
key: /* UNIQUE KEY TO IDENTIFY ITEM IN*/
price: /* Number */
quantity: /* number */
total: /*price*quantity*/
},
]
With the above cart structure you can update a single item, add any item or delete any item from the cart and you in the reducer basically you can iterate over the cart array and calculate the total price using the total key present in every object and then update total in the store.
I hope it helps. Thanks
Upvotes: 0