Search code examples
reactjsreact-nativereduxredux-toolkit

How to increase Items in Cart using REDUX


I am trying to increase the number of items in my cart using Redux (toolkit) in React native. I have created an add to basket reducer but upon clicking on the Plus button to increase the number of items, I get a line of the same item being duplicated below and I get a warning saying there are children with the same key.

BasketSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  items: [],
};

export const basketSlice = createSlice({
  name: "basket",
  initialState,
  reducers: {
    addToBasket: (state, action) => {
      state.items.push(action.payload);
    },
    removeFromBasket: (state, action) => {
      const index = state.items.findIndex(
        (basketItem) => basketItem.id === action.payload.id
      );
      let newBasket = [...state.items];
      if (index >= 0) {
        newBasket.splice(index, 1);
      } else {
        console.warn(
          `Cant remove product (id: ${action.payload.id}) as its not in the basket!`
        );
      }

      state.items = newBasket;
    },
  },
});

// Action creators are generated for each case reducer function
export const { addToBasket, removeFromBasket } =
  basketSlice.actions;

export const selectBasketItems = (state) => state.basket.items;

export default basketSlice.reducer;

ProductDetailScreen is the part where I can add an item to the basket

import { View, Text, Image, TouchableOpacity } from "react-native";
import React from "react";
import {  useRoute } from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons";
import BackButton from "../components/BackButton";
import { useDispatch } from "react-redux";
import { addToBasket } from "../../features/basketSlice";
import Toast from "react-native-toast-message";

const ProductDetailScreen = () => {
  const route = useRoute();
  const dispatch = useDispatch();
  const { id, image, name, price } = route.params;

  const addItemToCart = () => {
    dispatch(addToBasket({ id, image, name, price }));
    Toast.show({
      type: "success",
      text1: "Item added to cart!",
      position: "bottom",
    });
  };

  return (
    <View className="bg-white flex-1 justify-between">
      <Image
        source={{ uri: image }}
        className="h-auto w-full rounded-t-2xl flex-1 rounded-b-2xl"
        resizeMode="cover"
      />
      <BackButton />
     
        <TouchableOpacity
          onPress={addItemToCart}
          className="bg-indigo-500 mx-3 rounded-md p-3 flex-row justify-between mt-5 mb-1"
        >
          <View className="flex-row space-x-1">
            <Ionicons name="cart-sharp" size={30} color="white" />
            <Text className="text-white font-normal text-lg">Add to cart</Text>
          </View>
          <Text className="text-white font-extralight text-lg">|</Text>
          <Text className="text-white font-normal text-lg">${price}</Text>
        </TouchableOpacity>
      </View>
  );
};
export default ProductDetailScreen;

And the CartScreen.js

const CartScreen = () => {
  const dispatch = useDispatch();
  const items = useSelector(selectBasketItems);

  const handleDecrement = (id) => {
    dispatch(removeFromBasket({ id }));
  };

   const handleIncrement = (item) => {
dispatch(
  addToBasket({
    ...item,
    quantity: item.length + 1,
  })
);

};

  const totalPrice = items.reduce((total, item) => total + item.price, 0);

  return (
    <SafeAreaView className="flex-1 bg-white">

      {items.length === 0 ? (
        <View className="flex-1 items-center justify-center">
          <Text className="text-lg font-semibold">Your cart is empty</Text>
        </View>
      ) : (
        <View className="flex-1">
          {items.map((item) => (
            <View key={item.id} className="">
              <View className="flex-row my-1 mx-3">
                <View className="flex flex-row items-center">
                  <Image
                    source={{ uri: item.image }}
                    className="h-16 w-16 object-contain mr-2 rounded-md"
                  />
                  <View>
                    <Text className="text-lg font-semibold">{item.name}</Text>
                    <Text className="text-gray-400 font-bold">
                      $ {item.price}
                    </Text>
                  </View>
                </View>
                <View className="flex-1 flex flex-row justify-end items-end">
                  <TouchableOpacity
                    disabled={!items.length}
                    onPress={() => handleDecrement(item.id)}
                  >
                    <AntDesign name="minuscircleo" size={30} color="black" />
                  </TouchableOpacity>
                  <Text className="px-2">{items.length}</Text>
                  <TouchableOpacity onPress={() => handleIncrement(item)}>
                <AntDesign name="pluscircle" size={30} color="#757575" />
              </TouchableOpacity>
                </View>
              </View>
            </View>
          ))}
          <View></View>
          <View className="flex-row items-center justify-between px-6 py-5">
            <Text className="text-lg font-semibold">Total:</Text>
            <Text className="text-lg font-semibold">${totalPrice}</Text>
          </View>
          
        </View>
      )}
    </SafeAreaView>
  );
};

enter image description here


Solution

  • I had to add more variables in the initial state of the slice and upon adding an element to the basket, create an instance that would hold the quantity and there update it when an item is added or removed while at the same time calculating the total price. BasketSlice.js

    import { createSlice } from "@reduxjs/toolkit";
    
    export const basketSlice = createSlice({
      name: "basket",
      initialState: {
        items: [],
        totalPrice: 0,
      },
      reducers: {
        addToBasket: (state, action) => {
          const { id, image, name, price } = action.payload;
          const itemIndex = state.items.findIndex((item) => item.id === id);
          if (itemIndex !== -1) {
            state.items[itemIndex].quantity += 1;
          } else {
            state.items.push({ id, image, name, price, quantity: 1 });
          }
          state.totalPrice += price;
        },
    
        removeFromBasket: (state, action) => {
          const { id, price, quantity } = action.payload;
          state.items = state.items.filter((item) => item.id !== id);
          state.totalPrice -= price * quantity;
        },
    
        updateQuantity: (state, action) => {
          const { id, quantity } = action.payload;
          const itemIndex = state.items.findIndex((item) => item.id === id);
          state.items[itemIndex].quantity = quantity;
        },
    
        clearBasket: (state) => {
          state.items = [];
          state.totalPrice = 0;
        },
    
        updateTotalPrice: (state, action) => {
          state.totalPrice += action.payload;
        },
      },
    });
    
    export const {
      addToBasket,
      removeFromBasket,
      updateQuantity,
      clearBasket,
      updateTotalPrice,
    } = basketSlice.actions;
    
    export default basketSlice.reducer;
    

    And I applied the according changes on my CartScreen and now this is what it looks like

    CartScreen.js

    const CartScreen = () => {
      const dispatch = useDispatch();
      const { items, totalPrice } = useSelector((state) => state.basket);
    
      const handleRemoveItem = (id, price, quantity) => {
        dispatch(removeFromBasket({ id, price, quantity }));
      };
    
      const handleUpdateQuantity = (id, quantity, price) => {
        dispatch(updateQuantity({ id, quantity }));
    
        const item = items.find((item) => item.id === id);
        const prevQuantity = item.quantity;
        const newQuantity = quantity;
        const diffQuantity = newQuantity - prevQuantity;
        const itemPrice = price;
    
        dispatch({
          type: "basket/updateTotalPrice",
          payload: itemPrice * diffQuantity,
        });
      };
    
      const renderItem = ({ item }) => (
        <View className="flex-row my-1 mx-2 border-b border-gray-100 pb-3">
          <View className="flex flex-row items-center">
            <Image
              source={{ uri: item.image }}
              className="h-16 w-16 object-contain mr-2 rounded-md"
            />
            <View>
              <Text>{item.name}</Text>
              <Text>${item.price.toFixed(2)}</Text>
            </View>
          </View>
          <View className="flex-1 flex flex-row justify-end items-end space-x-3">
            <TouchableOpacity
              onPress={() =>
                handleUpdateQuantity(item.id, item.quantity - 1, item.price)
              }
              disabled={item.quantity === 1}
            >
              <AntDesign name="minuscircleo" size={25} color="black" />
            </TouchableOpacity>
            <Text className="text-lg">{item.quantity}</Text>
            <TouchableOpacity
              onPress={() =>
                handleUpdateQuantity(item.id, item.quantity + 1, item.price)
              }
            >
              <AntDesign name="pluscircle" size={25} color="#757575" />
            </TouchableOpacity>
            <TouchableOpacity
              onPress={() => handleRemoveItem(item.id, item.price, item.quantity)}
            >
              <AntDesign name="delete" size={24} color="red" />
            </TouchableOpacity>
          </View>
        </View>
      );
    
      return (
        <SafeAreaView className="flex-1 bg-white">
          
    
          <View className="flex-3">
            <FlatList
              data={items}
              renderItem={renderItem}
              keyExtractor={(item) => item.id.toString()}
              ListEmptyComponent={
                <Text className="text-lg font-semibold text-center ">
                  Your cart is empty
                </Text>
              }
            />
          </View>
         
        </SafeAreaView>
      );
    };