javascript 118 lines · 3 tabs

Persist and Hydrate a Shopping Cart with useReducer and localStorage

Shared by codesnips Jul 2026
3 tabs
export const initialCart = { items: {} };

export function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const { product, qty = 1 } = action;
      const existing = state.items[product.id];
      const nextQty = (existing ? existing.qty : 0) + qty;
      return {
        ...state,
        items: {
          ...state.items,
          [product.id]: { ...product, qty: nextQty },
        },
      };
    }
    case 'SET_QTY': {
      const { id, qty } = action;
      if (qty <= 0) {
        const { [id]: _removed, ...rest } = state.items;
        return { ...state, items: rest };
      }
      const item = state.items[id];
      if (!item) return state;
      return { ...state, items: { ...state.items, [id]: { ...item, qty } } };
    }
    case 'REMOVE_ITEM': {
      const { [action.id]: _removed, ...rest } = state.items;
      return { ...state, items: rest };
    }
    case 'HYDRATE':
      return { ...state, items: action.items };
    case 'CLEAR':
      return initialCart;
    default:
      return state;
  }
}
3 files · javascript Explain with highlit

This snippet shows a focused pattern for keeping a shopping cart in React state while transparently mirroring it to localStorage, so a page refresh or a returning visitor restores the same cart. The state lives in a useReducer, which is a good fit for a cart because every mutation (add, remove, change quantity, clear) is a discrete, well-typed action rather than an ad-hoc setState call scattered across components.

In cartReducer.js, the reducer is written as a pure function with a normalized shape: items are keyed by id in a plain object rather than an array, which makes ADD_ITEM and SET_QTY O(1) and avoids duplicate rows. ADD_ITEM merges quantities when the same product is added twice, SET_QTY filters out non-positive quantities so removing is just setting the count to zero, and every branch returns a brand-new object so React sees a referential change. Keeping the reducer pure and free of any localStorage calls is deliberate: it stays trivially testable and safe to run during server rendering.

The persistence lives in useCart.js. loadInitialState is passed as the third argument to useReducer (the lazy initializer), so reading and parsing storage happens exactly once, only in the browser. The typeof window guard means the same hook is safe under SSR, where window and localStorage do not exist. A try/catch around JSON.parse protects against corrupted or tampered data — a common real-world failure that would otherwise crash the app on boot. A useEffect writes the state back whenever it changes, debounced through a short setTimeout so rapid quantity clicks don't hammer synchronous storage writes. The useMemo derives totals so consumers get item count and price without recomputing on every render.

CartProvider.jsx wires the hook into context and exposes typed action creators like addItem and setQty, so components dispatch intent rather than raw action objects. A subtle but important detail is cross-tab sync: the storage event listener rehydrates the reducer when another tab mutates the cart, keeping multiple open tabs consistent. The main trade-off is that localStorage is synchronous and size-limited, so this pattern suits carts and preferences, not large datasets.


Related snips

typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs
typescript
import React from "react";

type FallbackProps = {
  error: Error;
  reset: () => void;
};

React Error Boundary + error reporting hook

react frontend error-boundary
by codesnips 3 tabs
javascript
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
  openAnalyzer: true,
});

/** @type {import('next').NextConfig} */

Next.js bundle analyzer for targeted performance work

nextjs performance tooling
by codesnips 4 tabs
typescript
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/contexts/AuthContext'

interface ProtectedRouteProps {
  children: React.ReactNode
}

React Router with protected routes

react react-router routing
by Maya Patel 2 tabs

Share this code

Here's the card — post it anywhere.

Persist and Hydrate a Shopping Cart with useReducer and localStorage — share card
Link copied