Architecture
Understanding how use-shopping-cart works internally in v4.0.
Overview
Version 4.0 represents a complete architectural overhaul while maintaining 100% backward compatibility. The library moved from Redux to React's native state management, resulting in a simpler, lighter, and more performant solution.
Redux Removal
Before (v3.x)
v3.x used Redux for state management:
CartProvider → Redux Store → Redux Reducer → Cart State
↓
React Context
↓
useShoppingCart HookDependencies:
reduxreact-redux@reduxjs/toolkit
After (v4.0)
v4.0 uses React's native useSyncExternalStore:
CartProvider → ShoppingCart Class → Pub/Sub Pattern → Cart State
↓
useSyncExternalStore
↓
useShoppingCart HookDependencies:
- None! (Just React 19+)
ShoppingCart Class
The core of v4.0 is the ShoppingCart class, which manages all cart state and operations.
Key Features
- Immutable State: All state updates create new objects
- Pub/Sub Pattern: Subscribers are notified of changes
- Persistence: Automatic localStorage sync
- Type-Safe: Full TypeScript support
Basic Structure
class ShoppingCart {
private state: CartState
private listeners: Set<Listener>
// Subscribe to changes
subscribe(listener: Listener): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
// Get current state
getSnapshot(): CartState {
return this.state
}
// Update cart
addItem(product: Product): void {
this.state = {
...this.state,
cartDetails: {
...this.state.cartDetails,
[product.id]: createCartEntry(product)
}
}
this.notifyListeners()
}
private notifyListeners(): void {
this.listeners.forEach((listener) => listener())
}
}React Integration
useSyncExternalStore
React 19's useSyncExternalStore connects components to the ShoppingCart:
function useShoppingCart() {
const cart = useContext(CartContext) // ShoppingCart instance
// Subscribe to cart changes
const state = useSyncExternalStore(cart.subscribe, cart.getSnapshot)
return {
...state,
addItem: cart.addItem,
removeItem: cart.removeItem
// ... other methods
}
}Benefits
- Automatic Re-renders: Components re-render when cart changes
- Selective Updates: Only components using changed data re-render
- No Provider Nesting: Single CartProvider at the root
- Concurrent Safe: Works with React 19 concurrent features
State Management Flow
Adding an Item
User clicks "Add to Cart"
↓
addItem(product)
↓
ShoppingCart.addItem()
↓
Update internal state
↓
Persist to localStorage
↓
Notify all subscribers
↓
Components re-renderState Immutability
All state updates create new objects:
// Wrong (mutates state)
this.state.cartDetails[id] = newItem
// Correct (creates new state)
this.state = {
...this.state,
cartDetails: {
...this.state.cartDetails,
[id]: newItem
}
}Persistence Layer
Storage Interface
interface Storage {
getItem(key: string): string | null
setItem(key: string, value: string): void
removeItem(key: string): void
}Built-in Adapters
- LocalStorage: Browser localStorage (default)
- MemoryStorage: In-memory (for SSR)
- NoopStorage: No persistence (testing)
Custom Storage
const customStorage = {
getItem: (key) => {
// Get from your storage
return sessionStorage.getItem(key)
},
setItem: (key, value) => {
// Save to your storage
sessionStorage.setItem(key, value)
},
removeItem: (key) => {
// Remove from your storage
sessionStorage.removeItem(key)
}
}
<CartProvider storage={customStorage} />Performance Optimizations
1. Memoized Methods
All cart methods are memoized to prevent unnecessary re-renders:
const addItem = useMemo(() => cart.addItem.bind(cart), [cart])2. Selective Subscriptions
Components only re-render when their used data changes:
// Only re-renders when cartCount changes
const { cartCount } = useShoppingCart()
// Only re-renders when totalPrice changes
const { totalPrice } = useShoppingCart()3. Computed Values
Values like formattedTotalPrice are computed on demand:
get formattedTotalPrice(): string {
return formatCurrencyString({
value: this.totalPrice,
currency: this.currency
})
}Bundle Size Comparison
| Version | Size (minified + gzipped) | Dependencies |
|---|---|---|
| v3.x | ~45 KB | Redux, React-Redux, RTK |
| v4.0 | ~27 KB | None (just React 19) |
| Savings | ~40% smaller | 3 fewer packages |
Migration Impact
No Code Changes Required
Thanks to careful API design, v4.0 is 100% backward compatible:
✅ All hooks work the same ✅ All methods have identical signatures ✅ All props are supported ✅ State structure is unchanged
What Changed Internally
- ❌ Redux store
- ❌ Redux actions
- ❌ Redux reducers
- ✅ ShoppingCart class
- ✅ useSyncExternalStore
- ✅ Pub/sub pattern
TypeScript Support
Type Inference
const { cartDetails } = useShoppingCart()
// TypeScript knows the exact shape
Object.values(cartDetails).forEach((item) => {
console.log(item.name) // ✓ string
console.log(item.quantity) // ✓ number
console.log(item.price) // ✓ number
})Exported Types
import type {
CartState,
CartConfig,
Product,
CartEntry,
CartDetails,
UseShoppingCartReturn
} from 'use-shopping-cart/core'Debugging
Dev Tools
import { DebugCart } from 'use-shopping-cart/react'
function App() {
return (
<CartProvider>
<DebugCart />
<YourApp />
</CartProvider>
)
}Shows:
- Current cart state
- Cart actions
- State changes in real-time
Logging
Enable logging in development:
const cart = new ShoppingCart(config)
cart.subscribe(() => {
console.log('Cart updated:', cart.getSnapshot())
})Benefits of New Architecture
1. Simpler Mental Model
No need to understand Redux concepts:
- No actions
- No reducers
- No dispatch
- No selectors
Just: "Call a method, state updates, components re-render"
2. Better Performance
- Fewer dependencies
- Smaller bundle
- Faster initialization
- More efficient updates
3. React 19 Native
- Uses React's built-in features
- Works with concurrent rendering
- Supports React Server Components
- Future-proof
4. Easier Testing
// No Redux setup needed
const cart = new ShoppingCart(config)
cart.addItem(product)
expect(cart.getSnapshot().cartCount).toBe(1)
cart.removeItem(product.id)
expect(cart.getSnapshot().cartCount).toBe(0)See Also
- Migration Guide - Upgrade from v3.x
- useShoppingCart Hook - Main API
