skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

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

  2. The problem is how you wrote your reducer function in BasketSlice.js. Right now it pushes the payload item whenever you dispatch an addtoBasket action . Instead what it should ideally do is check if an item with id is already in cart , if yes it should increment the quantity of the item , if item not present in cart it should insert into the array with quantity as one

    BasketSlice.js

    ...
    reducers: {
     addToBasket: (state, action) => {
      const itemInCart = state.items.find((item) => item.id === 
       action.payload.id);
      if (itemInCart) {
        itemInCart.quantity++;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
    },
    ...
    

    Also change the how quantity is shown in CartScreen as follows

    CartScreen.js

    ...
    <TouchableOpacity
       disabled={!items.length}
       onPress={() => handleDecrement(item.id)}
       >
        <AntDesign name="minuscircleo" size={30} color="black" />
    </TouchableOpacity>
    <Text className="px-2">{item.quantity}</Text>   //<=== change here
    <TouchableOpacity onPress={() => handleIncrement(item.id)}>
       <AntDesign name="pluscircle" size={30} color="#757575" />
    </TouchableOpacity>
    ...
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search