Whether you're using Redux Toolkit, Zustand, React's Context API, or any other global state management solution for your React.ts e-commerce store, the fundamental approach to building a robust shopping cart remains the same. Make it fool-proof, informative and do what the user expects it to do.
Building a sensible shopping cart starts with your products themselves. Products need to have the following key features:
Unique Identifier (ID) – Each product should have a unique identifier to distinguish it from others in the catalog. This ID will help manage the products in the cart, ensuring you can correctly reference and manipulate each one.
Name and Description – Every product should include a name and a short description to help users identify what they’re purchasing. These details make the shopping experience clearer and more user-friendly.
Price – The product’s price must be easily accessible. This is essential for calculating the total cost in the cart, applying discounts, and showing users an accurate breakdown of their expenses. You may also wish to store historical price data for features such as sales.
Stock – Keeping track of stock levels is critical for ensuring the availability of products. Managing stock effectively allows the cart to display accurate product availability and prevents users from adding items that are out of stock.
Image – An image adds to the clarity and attractiveness of the product page and cart. If you're storing multiple images per product, this will be an array. The image is best stored as a URL.
Date of Creation - Its usually good practice to store the date when the product was created for use later on with filtering and sorting mechanisms.
Attributes (Optional) – Products may have attributes such as available sizes or colors. Allowing users to select and modify these attributes within the cart ensures they get the exact product they need.
In TypeScript you could type the Product object as follows:
types.ts
type Size = "S" | "M" | "L" | "XL";
type Color = {
label: string;
hex: string;
};
type Product = {
id: number;
name: string;
description: string;
category: string;
price: number;
stock: number;
image: string;
created_at: Date;
attributes: {
sizes: Size[];
colors: Color[];
};
};
Great, our Product object is now set up with all the essential fields. Next, let’s focus on the Cart. To minimise the amount of data stored locally in state, we'll reference the Product's ID and only retain the necessary fields:
Unique Identifier (ID) – Similar to products, cart items require a unique identifier to differentiate between items that share the same product ID but have different attributes, such as size or color.
Selected Attributes – It's essential to accurately track the user's selected product attributes, such as color and size, to ensure a seamless checkout.
Quantity – The number of items the user wants to purchase must be tracked. This allows the cart to accurately reflect the total cost and inventory levels, ensuring the correct quantity is available for checkout.
Reference to the Product's ID – Each cart item must reference the original product ID to ensure it's connected to the correct product details, such as name, description, and price.
types.ts
type CartItem = {
id: number; // a new id to distinguish products with the same product id but different attributes
size: Size;
color: Color;
quantity: number;
product_id: number; // reference to the product's id for retrieving product data
};
type Cart = {
items: CartItem[];
};
Once the product and cart objects are properly set up, the next step is to handle cart operations like adding items, updating attributes, and removing items. Whether you're using Redux Toolkit, Zustand, or React's Context API, the logic follows the same fundamental principles.
When adding an item to the cart, first check whether an item with the same product ID and attributes (like size and color) already exists. If it does, simply increase the quantity. If not, create a new CartItem and append it to the cart.
Steps:
actions.ts
function addToCart(state: Cart, newItem: CartItem): Cart {
const existingItem = state.items.find(
(item) =>
item.product_id === newItem.product_id &&
item.size === newItem.size &&
item.color.hex === newItem.color.hex
);
if (existingItem) {
return {
...state,
items: state.items.map((item) =>
item.id === existingItem.id
? { ...item, quantity: item.quantity + newItem.quantity }
: item
),
};
} else {
return {
...state,
items: [...state.items, { ...newItem, id: Date.now() }], // Generates a new unique ID
};
}
}
Sometimes users may want to update the quantity or attributes of an item in the cart, such as changing the size or color. In this case, you'll need to find the item, update its attributes or quantity. It's also crucial to review the entire cart after making these changes to determine if any additional actions are needed.
Steps:
actions.ts
function updateCartItem(
state: Cart,
id: number,
newAttributes: { size?: Size; color?: Color; quantity?: number }
): Cart {
return {
...state,
items: state.items.map((item) =>
item.id === id ? { ...item, ...newAttributes } : item
),
};
}
EDGE CASE: Let's say a user has the following two cart items in their cart.
actions.test.ts
const CartItem1 = {
id: 1,
size: "L",
color: "Violet",
quantity: 2,
product_id: 58,
};
const CartItem2 = {
id: 2,
size: "M",
color: "Violet",
quantity: 1,
product_id: 58,
};
What would happen if we were to update the size of CartItem1 to "M"?
In the current implementation of the updateCartItem function, we would end up with two separate cart items that have exactly the same properties except for their id.
SOLUTION: We need to create a reusable function that iterates through the cart items and checks for matches based on the product_id, size, and color fields. If matching items are found, their quantities should be consolidated into a single cart item. This function can be called at the end of the updateCartItem function.
actions.ts
function consolidateCartItems(state: Cart): Cart {
const consolidatedItems: CartItem[] = [];
// Iterate over each cart item
state.items.forEach((item) => {
// Check if item already exists in consolidatedItems
const existingItem = consolidatedItems.find(
(i) =>
i.product_id === item.product_id &&
i.size === item.size &&
i.color.hex === item.color.hex
);
if (existingItem) {
// If found, update the quantity
existingItem.quantity += item.quantity;
} else {
// If not found, add the new item to consolidatedItems
consolidatedItems.push({ ...item });
}
});
// Return the updated cart with consolidated items
return {
...state,
items: consolidatedItems,
};
}
To remove an item, simply filter it out of the cart by its unique ID.
Steps:
actions.ts
function removeFromCart(state: Cart, itemId: number) {
return {
...state,
items: state.items.filter((item) => item.id !== itemId),
};
}
It's important to provide users with real-time updates on their cart's total price and quantity. Whenever a change is made to the cart, call these functions.
actions.ts
function calculateTotalPrice(state: Cart, products: Product[]) {
return state.items.reduce((total, item) => {
const product = products.find((p) => p.id === item.product_id);
return total + (product ? product.price * item.quantity : 0);
}, 0);
}
function calculateTotalQuantity(state: Cart) {
return state.items.reduce((total, item) => total + item.quantity, 0);
}
By following these principles, you can build a flexible and user-friendly shopping cart system for your e-commerce application. Remember to keep the user experience in mind, ensuring that the cart is easy to use, intuitive, and responsive to user actions. Whether you’re dealing with state management through Redux, Zustand, or Context API, the fundamentals remain the same: make it clear, concise, and reliable.
Tuesday, September 10th, 2024