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;
}
}
import { useReducer, useEffect, useMemo, useRef } from 'react';
import { cartReducer, initialCart } from './cartReducer';
const STORAGE_KEY = 'cart:v1';
function loadInitialState() {
if (typeof window === 'undefined') return initialCart;
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return initialCart;
const parsed = JSON.parse(raw);
return parsed && typeof parsed.items === 'object'
? { items: parsed.items }
: initialCart;
} catch (err) {
console.warn('Failed to parse persisted cart, starting fresh', err);
return initialCart;
}
}
export function useCart() {
const [state, dispatch] = useReducer(cartReducer, undefined, loadInitialState);
const writeTimer = useRef(null);
useEffect(() => {
if (typeof window === 'undefined') return;
clearTimeout(writeTimer.current);
writeTimer.current = setTimeout(() => {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, 200);
return () => clearTimeout(writeTimer.current);
}, [state]);
useEffect(() => {
function onStorage(e) {
if (e.key !== STORAGE_KEY || !e.newValue) return;
try {
const parsed = JSON.parse(e.newValue);
dispatch({ type: 'HYDRATE', items: parsed.items || {} });
} catch (_) {}
}
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);
const totals = useMemo(() => {
const items = Object.values(state.items);
return {
count: items.reduce((n, it) => n + it.qty, 0),
subtotal: items.reduce((n, it) => n + it.qty * it.price, 0),
};
}, [state.items]);
return { state, dispatch, totals };
}
import { createContext, useContext, useMemo } from 'react';
import { useCart } from './useCart';
const CartContext = createContext(null);
export function CartProvider({ children }) {
const { state, dispatch, totals } = useCart();
const value = useMemo(() => ({
items: Object.values(state.items),
totals,
addItem: (product, qty = 1) => dispatch({ type: 'ADD_ITEM', product, qty }),
setQty: (id, qty) => dispatch({ type: 'SET_QTY', id, qty }),
removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', id }),
clear: () => dispatch({ type: 'CLEAR' }),
}), [state.items, totals, dispatch]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
export function useCartContext() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCartContext must be used within a CartProvider');
return ctx;
}
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
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
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
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
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
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
import { Navigate, useLocation } from 'react-router-dom'
import { useAuth } from '@/contexts/AuthContext'
interface ProtectedRouteProps {
children: React.ReactNode
}
React Router with protected routes
Share this code
Here's the card — post it anywhere.