use-shopping-cart logouse-shopping-cart
Concepts

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 Hook

Dependencies:

  • redux
  • react-redux
  • @reduxjs/toolkit

After (v4.0)

v4.0 uses React's native useSyncExternalStore:

CartProvider → ShoppingCart Class → Pub/Sub Pattern → Cart State

              useSyncExternalStore

              useShoppingCart Hook

Dependencies:

  • None! (Just React 19+)

ShoppingCart Class

The core of v4.0 is the ShoppingCart class, which manages all cart state and operations.

Key Features

  1. Immutable State: All state updates create new objects
  2. Pub/Sub Pattern: Subscribers are notified of changes
  3. Persistence: Automatic localStorage sync
  4. 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

  1. Automatic Re-renders: Components re-render when cart changes
  2. Selective Updates: Only components using changed data re-render
  3. No Provider Nesting: Single CartProvider at the root
  4. 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-render

State 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

  1. LocalStorage: Browser localStorage (default)
  2. MemoryStorage: In-memory (for SSR)
  3. 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

VersionSize (minified + gzipped)Dependencies
v3.x~45 KBRedux, React-Redux, RTK
v4.0~27 KBNone (just React 19)
Savings~40% smaller3 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

Shopping Cart

Your cart is empty

Try the interactive demos on the docs pages!