javascriptreactjsreduxincrementdecrement

Increment/decrement button React.js/Redux


I know similar questions have been asked, but I couldn't find one which I could adapt to my code (possibly cause I am still new to Redux). So, I am editing the cart functionality which was already written and where I already have two functions:

  1. Update the quantity number whenever user adds or removes product(s).
  2. Update the total price of added products. At the moment, quantity number is displayed via input with its default arrows (therefore it's not very touchscreen-friendly), and the user can't really delete the starting number 1 in order to 'manually' change the quantity by typing in. My job is to make increment and decrement buttons along with different input where user will also be able to type in the desired quantity. How can I do this?
export function calculateTotal(cart) {
    let totalUSD = 0;
    Object.keys(cart).forEach((itemName) => {
      totalUSD += cart[itemName].price * cart[itemName].quantity;
    });
    return totalUSD.toFixed(2);
  }
  export function calculateTotal(cart) {
    let totalUSD = 0;
    Object.keys(cart).forEach((itemName) => {
      totalUSD += cart[itemName].price * cart[itemName].quantity;
    });
    return totalUSD.toFixed(2);
  }
  export function calculateTotalQuantity(cart) {
    let totalQuantity = 0;
    Object.keys(cart).forEach((itemName) => {
      totalQuantity += cart[itemName].quantity;
    });
    return totalQuantity;
  }
import React from 'react';
import "./Cart.css";
import Navbar from './Navbar.jsx';
import Footer from './Footer.jsx';
import { useDispatch, useSelector } from 'react-redux';
import { changeItemQuantity } from '../features/cart/cartSlice.js';
import { calculateTotal } from '../features/utilities/utilities.js';
import { removeAll } from '../features/cart/cartSlice.js';

export default function Cart() {

  const dispatch = useDispatch();
  const cart = useSelector(state => state.cart); // * Note
  const onInputChangeHandler = (name, input) => {
    // If the user enters a bad value...
    if (input === '') {
      return;
    }

    // Otherwise, convert the input into a number and pass it along as the newQuantity.
   const newQuantity = Number(input);

    dispatch(changeItemQuantity(name, newQuantity));

  };

  const onRemoveAll = () => {

    dispatch(removeAll());
  };
  // Use the cart and currencyFilter slices to render their data.
  const cartElements = Object.entries(cart).map(createCartItem);
  const total = calculateTotal(cart);
  
  return (
    <>
      <Navbar/>
      <div id="cart-container">
        <ul id="cart-items">
          {cartElements == '' ? <div className="cart-empty">Your cart is empty</div> : cartElements}
        </ul>
        <div className="price-btn-container">
          {cartElements.length > 0 ? <button
              onClick={() => onRemoveAll()}
              className="remove-all"
            >
              Remove All
            </button> : <div></div>}
          <h3 className="total">
            Total{' '}
            <span className="total-value">
              ${total} 
            </span>
          </h3>
        </div>
      </div>
      <Footer/>
    </>
  );

  function createCartItem([name, item]) {
    if (item.quantity === 0) {
      return;
    }

    return (
      <li className="cart-list" key={name}>
        <img src={item.img}/>
        <p>{name}</p>
        <p>{item.type}</p>
        <div className="quantity-container">
          <p>Quantity:</p>
          <input 
          type="number" 
          className="item-quantity" 
          name="quantity" 
          value={item.quantity}
          onChange={(e) => {
            onInputChangeHandler(name, e.target.value);
          }}/>
        </div>
      </li>
    );
  }
};
export const addItem = (itemToAdd) => {
    return {
      type: 'cart/addItem',
      payload: itemToAdd,
    };
  };

  export const removeAll = () => {
    return {
      type: 'cart/removeAll',
      payload: initialCart
    }

  }
  
  export const changeItemQuantity = (name, newQuantity ) => {
   return {
      type: 'cart/changeItemQuantity',
      payload: {
        name,
        newQuantity
      }
    }
  }
  
  
  const initialCart = [];
  export const cartReducer = (cart = initialCart, action) => {
    switch (action.type) {
      case 'cart/addItem': {
        const { name, price, img, type } = action.payload;
        
        // if the item already exists, increase the quantity by 1, otherwise set it to 1
        const quantity = cart[name] ? cart[name].quantity + 1 : 1;
        const newItem = { price, img, type, quantity };
  
        // Add the new item to the cart (or replace it if it existed already)
        return { 
          ...cart, 
          [name]: newItem 
        };
      }
      case 'cart/removeItem':
        return cart.filter(item => item !== action.payload);
        
      case 'cart/removeAll':
        return initialCart;

      case 'cart/changeItemQuantity': {
        const { name, newQuantity } = action.payload;
        const itemToUpdate = cart[name];
        const updatedItem = {
          ...itemToUpdate,
          quantity: newQuantity
        }
        // Create a copy of itemToUpdate and update the quantity prop.
        return {
          ...cart,
          [name]: updatedItem 
        }
      }
      default: {
        return cart;
      }
    }
  };

Solution

  • Create two new actions/reducer cases to increment/decrement the item quantity.

    export const addItem = (itemToAdd) => ({
      type: 'cart/addItem',
      payload: itemToAdd,
    });
    
    export const removeAll = () => ({
      type: 'cart/removeAll',
      payload: initialCart
    });
      
    export const changeItemQuantity = (name, newQuantity) => ({
      type: 'cart/changeItemQuantity',
      payload: { name, newQuantity }
    });
    
    export const const incrementItemQuantity = (name) => ({
      type: 'cart/incrementItemQuantity',
      payload: name
    });
    
    export const const decrementItemQuantity = (name) => ({
      type: 'cart/decrementItemQuantity',
      payload: name
    });
      
    const initialCart = {};
    
    export const cartReducer = (cart = initialCart, action) => {
      switch (action.type) {
        case 'cart/addItem': {
          const { name, price, img, type } = action.payload;
            
          // if the item already exists, increase the quantity by 1, otherwise set it to 1
          const quantity = cart[name] ? cart[name].quantity + 1 : 1;
          const newItem = { price, img, type, quantity };
      
          // Add the new item to the cart (or replace it if it existed already)
          return { 
            ...cart, 
            [name]: newItem 
          };
        }
    
        case 'cart/removeItem':
          return cart.filter(item => item !== action.payload);
            
        case 'cart/removeAll':
          return initialCart;
    
        case 'cart/changeItemQuantity': {
          const { name, newQuantity } = action.payload;
    
          return {
            ...cart,
            [name]: {
              ...cart[name],
              quantity: newQuantity
            } 
          }
        }
    
        case 'cart/incrementItemQuantity':
          const name = action.payload;
          return {
            ...cart,
            [name]: {
              ...cart[name],
              quantity: cart[name].quantity + 1
            } 
          }
    
        case 'cart/decrementItemQuantity':
          const name = action.payload;
          return {
            ...cart,
            [name]: {
              ...cart[name],
              quantity: Math.max(1, cart[name].quantity - 1)
            } 
          }
    
        default:
          return cart;
      }
    };
    
    import React from 'react';
    import "./Cart.css";
    import Navbar from './Navbar.jsx';
    import Footer from './Footer.jsx';
    import { useDispatch, useSelector } from 'react-redux';
    import {
      changeItemQuantity,
      decrementItemQuantity,
      incrementItemQuantity,
      removeAll
    } from '../features/cart/cartSlice.js';
    import { calculateTotal } from '../features/utilities/utilities.js';
    
    export default function Cart() {
      const dispatch = useDispatch();
      const cart = useSelector(state => state.cart); // * Note
    
      const onInputChangeHandler = (name, input) => {
        // If the user enters a bad value...
        if (input === '') {
          return;
        }
    
        // Otherwise, convert the input into a number and pass it along as the newQuantity.
        const newQuantity = Number(input);
    
        dispatch(changeItemQuantity(name, newQuantity));
      };
    
      const onRemoveAll = () => dispatch(removeAll());
      const incrementItem = (name) => dispatch(incrementItemQuantity(name));
      const decrementItem = (name) => dispatch(decrementItemQuantity(name));
    
      function createCartItem([name, item]) {
        return (
          <li className="cart-list" key={name}>
            <img src={item.img}/>
            <p>{name}</p>
            <p>{item.type}</p>
            <div className="quantity-container">
              <p>Quantity:</p>
              <input 
                type="number" 
                className="item-quantity" 
                name="quantity" 
                value={item.quantity}
                onChange={(e) => {
                  onInputChangeHandler(name, e.target.value);
                }}
              />
              <button type="button" onClick={decrementItem.bind(null, name)}>
                -
              </button>
              <button type="button" onClick={incrementItem.bind(null, name)}>
                +
              </button>
            </div>
          </li>
        );
      }
      
      // Use the cart and currencyFilter slices to render their data.
      const cartElements = Object.entries(cart)
        .filter(([,item]) => item.quantity)
        .map(createCartItem);
    
      const total = calculateTotal(cart);
    
      return (
        <>
          <Navbar />
          <div id="cart-container">
            {cartElements.length
              ? (
                <ul id="cart-items">
                  {cartElements}
                </ul>
              ) : <div className="cart-empty">Your cart is empty</div>
            }
            <div className="price-btn-container">
              {!!cartElements.length && ( // <-- coerce length to boolean
                <button
                  onClick={onRemoveAll}
                  className="remove-all"
                >
                  Remove All
                </button>
              )}
              <h3 className="total">
                Total{' '}
                <span className="total-value">
                  ${total} 
                </span>
              </h3>
            </div>
          </div>
          <Footer />
        </>
      );
    };
    

    If you don't want the extra actions/reducers, you could handle it with the changeItemQuantity action.

    case 'cart/changeItemQuantity': {
      const { name, newQuantity } = action.payload;
    
      return {
        ...cart,
        [name]: {
          ...cart[name],
          quantity: Math.max(1, newQuantity),
        } 
      }
    }
    
    import React from 'react';
    import "./Cart.css";
    import Navbar from './Navbar.jsx';
    import Footer from './Footer.jsx';
    import { useDispatch, useSelector } from 'react-redux';
    import {
      changeItemQuantity,
      removeAll
    } from '../features/cart/cartSlice.js';
    import { calculateTotal } from '../features/utilities/utilities.js';
    
    export default function Cart() {
      const dispatch = useDispatch();
      const cart = useSelector(state => state.cart); // * Note
    
      const onInputChangeHandler = (name, input) => {
        // If the user enters a bad value...
        if (input === '') {
          return;
        }
    
        // Otherwise, convert the input into a number and pass it along as the newQuantity.
        const newQuantity = Number(input);
    
        dispatch(changeItemQuantity(name, newQuantity));
      };
    
      const onRemoveAll = () => dispatch(removeAll());
      const incrementItem = (name, item) => dispatch(
        changeItemQuantity(name, item.quantity + 1)
      );
      const decrementItem = (name, item) => dispatch(
        changeItemQuantity(name, item.quantity - 1)
      );
    
      function createCartItem([name, item]) {
        return (
          <li className="cart-list" key={name}>
            <img src={item.img}/>
            <p>{name}</p>
            <p>{item.type}</p>
            <div className="quantity-container">
              <p>Quantity:</p>
              <input 
                type="number" 
                className="item-quantity" 
                name="quantity" 
                value={item.quantity}
                onChange={(e) => {
                  onInputChangeHandler(name, e.target.value);
                }}
              />
              <button type="button" onClick={decrementItem.bind(null, name, item)}>
                -
              </button>
              <button type="button" onClick={incrementItem.bind(null, name, item)}>
                +
              </button>
            </div>
          </li>
        );
      }
      
      // Use the cart and currencyFilter slices to render their data.
      const cartElements = Object.entries(cart)
        .filter(([,item]) => item.quantity)
        .map(createCartItem);
    
      const total = calculateTotal(cart);
    
      return (
        <>
          <Navbar />
          <div id="cart-container">
            {cartElements.length
              ? (
                <ul id="cart-items">
                  {cartElements}
                </ul>
              ) : <div className="cart-empty">Your cart is empty</div>
            }
            <div className="price-btn-container">
              {!!cartElements.length && (
                <button
                  onClick={onRemoveAll}
                  className="remove-all"
                >
                  Remove All
                </button>
              )}
              <h3 className="total">
                Total{' '}
                <span className="total-value">
                  ${total} 
                </span>
              </h3>
            </div>
          </div>
          <Footer />
        </>
      );
    };