简体   繁体   中英

In React/Redux, how to calculate a Total price for a shopping cart

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.

Action:

/**
 * @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
  });
};

Reducer:

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

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.

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

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

Action

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

Reducer

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.

I do not use Redux but calculating total price with tax is pure javascript. In the beginning you should have "total" property set to 0 in your items object. item.total would be 0.

state = {
    products: [],
    cart: [], // you are populating cart in "addToCart"  method
    cartSubTotal: 0,
    cartTax: 0,
    cartTotal: 0
  };
addTotals = () => {
    let subTotal = 0;
    this.state.cart.map(item => (subTotal += item.total)); 
    const tempTax = subTotal * 0.1;  //10% tax 
    const tax = parseFloat(tempTax.toFixed(2)); //limiting tax to 2 decimal numbers
    const total = subTotal + tax;
    this.setState(() => {
      return {
        cartSubTotal: subTotal,
        cartTax: tax,
        cartTotal: total
      };
    });
  };

You should also call "addTotals" method, in your "addToCard" method when setting the state. I will just show how to implement it in "this.setState" method.

addToCart method

this.setState(
      () => {
        return {
          products: [tempProducts],
          cart: [...this.state.cart, product],
          detailProduct: { ...product }
        };
      },
      () => this.addTotals()
    );

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM