How to build an e-commerce cart in React
Building a shopping cart requires managing shared state across multiple components — the cart icon in the header, the product page, and the checkout summary all need access to the same cart data.
As the creator of CoreUI with 25 years of front-end development experience, I’ve implemented cart systems for numerous e-commerce projects and the most maintainable solution uses React Context with useReducer for predictable state updates.
This avoids prop drilling and keeps cart logic centralized and testable.
Once the cart context is set up, any component in the tree can read or update the cart with a simple hook.
Define the cart reducer and context.
// CartContext.jsx
import { createContext, useContext, useReducer } from 'react'
const CartContext = createContext(null)
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(i => i.id === action.item.id)
if (existing) {
return {
...state,
items: state.items.map(i =>
i.id === action.item.id
? { ...i, quantity: i.quantity + 1 }
: i
)
}
}
return { ...state, items: [...state.items, { ...action.item, quantity: 1 }] }
}
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.id) }
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(i =>
i.id === action.id ? { ...i, quantity: action.quantity } : i
).filter(i => i.quantity > 0)
}
case 'CLEAR_CART':
return { ...state, items: [] }
default:
return state
}
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] })
const total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
const itemCount = state.items.reduce((sum, i) => sum + i.quantity, 0)
return (
<CartContext.Provider value={{ ...state, total, itemCount, dispatch }}>
{children}
</CartContext.Provider>
)
}
export function useCart() {
const ctx = useContext(CartContext)
if (!ctx) throw new Error('useCart must be used within CartProvider')
return ctx
}
useReducer is ideal for cart logic because each action type has a predictable, testable transformation. The reducer returns a new state object without mutating the original. Computed values like total and itemCount are derived on every render from the items array.
Add to Cart Button
Dispatch the ADD_ITEM action from a product component.
// ProductCard.jsx
import { useCart } from './CartContext'
export function ProductCard({ product }) {
const { dispatch } = useCart()
function addToCart() {
dispatch({ type: 'ADD_ITEM', item: product })
}
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price.toFixed(2)}</p>
<button onClick={addToCart}>Add to Cart</button>
</div>
)
}
The product component doesn’t manage cart state — it simply dispatches an action. This means the same dispatch call works identically from a product list, a product detail page, or a recommended items widget.
Cart Summary Component
Display cart items with quantity controls.
// CartSummary.jsx
import { useCart } from './CartContext'
export function CartSummary() {
const { items, total, dispatch } = useCart()
if (items.length === 0) {
return <p>Your cart is empty.</p>
}
return (
<div className="cart">
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<input
type="number"
min="0"
value={item.quantity}
onChange={(e) =>
dispatch({
type: 'UPDATE_QUANTITY',
id: item.id,
quantity: Number(e.target.value)
})
}
/>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>
Remove
</button>
</div>
))}
<div className="cart-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
</div>
)
}
Setting min="0" on the quantity input combined with the reducer’s filter(i => i.quantity > 0) means setting quantity to zero removes the item automatically. This keeps the UI interaction intuitive.
Best Practice Note
This is the same state management pattern we use in CoreUI React e-commerce templates. For production carts, persist the cart to localStorage by adding a useEffect that syncs state on every change, and restore it on mount. If your cart needs server synchronization (for logged-in users), replace the local state with server state using React Query or SWR. When you’re ready to build the checkout page, see how to build a checkout page in React.



