Search code examples
javascriptreactjsreduxreact-reduxredux-toolkit

react redux toolkit how to reflect changes of every single items price after change the quantity?


Right now I can reflect changes of total price if quantity changes. here is an example what I am doing now:

price of every single item 
quantity {2} {200}
quantity {2} {100}
    
total: price = 600

Here is my cart slice:

const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    total: 0,
  },
  reducers: {
    addItem(state, action) {
      const { id, title, price, size, color, image, quantity = 1 } = action.payload;
      const existingItem = state.items.find(item => item.id === id);
      if (existingItem) {
        existingItem.quantity += quantity;
      } else {
        state.items.push({ id, title, price, size, color, image, quantity });
      }
      state.total += price * quantity;
      
    },
    removeItem(state, action) {
      const { id, quantity = state.quantity } = action.payload;
      const existingItem = state.items.find(item => item.id === id);
      if (existingItem) {
        if (existingItem.quantity > quantity) {
          existingItem.quantity -= quantity;
        } else {
          state.items = state.items.filter(item => item.id !== id);
        }
        state.total -= existingItem.price * quantity;
      }
    },
    removeSingleItem(state, action) {
      const { id, quantity = 1 } = action.payload;
      const existingItem = state.items.find(item => item.id === id);
      if (existingItem) {
        if (existingItem.quantity > quantity) {
          existingItem.quantity -= quantity;
        } else {
          state.items = state.items.filter(item => item.id !== id);
        }
        state.total -= existingItem.price * quantity;
      }
    },
  },
});

export const {
  addItem,
  removeItem,
  addSingleItem,
  removeSingleItem
} = cartSlice.actions;
export default cartSlice.reducer;

My expected result will be:

price of every single item 
quantity {2} {400}
quantity {2} {200}
    
total: price = 600

Solution

  • React state should be the minimum data it takes to represent the "state"; it's considered a bit of a React anti-pattern to store derived state in state. In this case a cart total can be computed directly from the state.cart.items array.

    Example:

    const items = useSelector(state => state.cart.items);
    
    const total = items.reduce((total, { price = 0, quantity = 0 }) => {
      return total + price * quantity;
    }, 0);
    

    If you want or need to you can memoize the total using the useMemo hook.

    const items = useSelector(state => state.cart.items);
    
    const total = useMemo(() => {
      return items.reduce((total, { price = 0, quantity = 0 }) => {
        return total + price * quantity;
      }, 0);
    }, [items]);
    

    redux-toolkit also exports reselect functions. You could use createSelector to create a memoized selector function. In the cartSlice file create an input selector to select the state.cart.items array and then create the memoized selector`.

    import { createSlice, createSelector } from '@reduxjs/toolkit';
    
    const cartSlice = createSlice({
      ...
    });
    
    export const cartItemsSelector = state => state.cart.items;
    
    export const cartTotalSelector = createSelector(
      [cartItemsSelector],
      items => items.reduce((total, { price = 0, quantity = 0 }) => {
        return total + price * quantity;
      }, 0)
    );
    
    const items = useSelector(cartItemsSelector);
    const total = useSelector(cartTotalSelector);
    
    ...
    

    Remove the total state and logic from the cart slice.

    const cartSlice = createSlice({
      name: 'cart',
      initialState: {
        items: [],
      },
      reducers: {
        addItem(state, action) {
          const { id } = action.payload;
          const existingItem = state.items.find(item => item.id === id);
    
          if (existingItem) {
            existingItem.quantity += quantity;
          } else {
            state.items.push({ ...action.payload, quantity: 1 });
          }
        },
        removeItem(state, action) {
          const { id, quantity = 1 } = action.payload;
          const existingItem = state.items.find(item => item.id === id);
    
          if (existingItem) {
            if (existingItem.quantity > quantity) {
              existingItem.quantity -= quantity;
            } else {
              state.items = state.items.filter(item => item.id !== id);
            }
          }
        },
        removeSingleItem(state, action) {
          const { id, quantity = 1 } = action.payload;
          const existingItem = state.items.find(item => item.id === id);
          if (existingItem) {
            if (existingItem.quantity > quantity) {
              existingItem.quantity -= quantity;
            } else {
              state.items = state.items.filter(item => item.id !== id);
            }
          }
        },
      },
    });
    
    export const {
      addItem,
      removeItem,
      addSingleItem,
      removeSingleItem
    } = cartSlice.actions;
    
    export default cartSlice.reducer;