Persist a React Shopping Cart to localStorage with a Context Provider

Shared by codesnips Jul 2026
3 tabs
import { useState, useEffect, useCallback } from 'react';

export function useLocalStorage(key, initialValue) {
  const readValue = useCallback(() => {
    if (typeof window === 'undefined') return initialValue;
    try {
      const raw = window.localStorage.getItem(key);
      return raw ? JSON.parse(raw) : initialValue;
    } catch (err) {
      console.warn(`useLocalStorage: failed to read "${key}"`, err);
      return initialValue;
    }
  }, [key, initialValue]);

  const [value, setValue] = useState(readValue);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (err) {
      console.warn(`useLocalStorage: failed to write "${key}"`, err);
    }
  }, [key, value]);

  useEffect(() => {
    const onStorage = (event) => {
      if (event.key === key) setValue(readValue());
    };
    window.addEventListener('storage', onStorage);
    return () => window.removeEventListener('storage', onStorage);
  }, [key, readValue]);

  return [value, setValue];
}
3 files · javascript Explain with highlit

A shopping cart is a classic case where React state must survive page reloads and stay consistent across tabs, so this snippet combines a reducer-driven Context provider with a small localStorage synchronization hook. The design keeps the reducer pure and framework-agnostic while pushing all the messy browser concerns — reading on mount, writing on change, and reacting to changes from other tabs — into a reusable hook.

In useLocalStorage hook, the value is lazily initialized so the expensive JSON.parse only runs once, not on every render. The initializer is wrapped in a try/catch because localStorage.getItem can throw in private-browsing modes and stored JSON can be corrupt; on any failure it falls back to initialValue rather than crashing the app. A useEffect writes the serialized value back whenever it changes, and a storage event listener keeps the state in sync when a different tab mutates the same key — the browser only fires storage in other tabs, which is exactly the cross-tab behavior that is wanted. Guarding on typeof window makes the hook safe to import under server-side rendering, where window and localStorage do not exist.

In cartReducer, the cart is modeled as a flat array of line items keyed by id. ADD_ITEM is written to be idempotent about quantity: if the item already exists it increments quantity instead of pushing a duplicate row, which is the common source of "why do I have the same product twice" bugs. REMOVE_ITEM filters by id, SET_QTY clamps the quantity to a minimum of one and drops the line entirely when it would hit zero, and CLEAR resets to an empty array. Keeping this logic in a pure function means it can be unit-tested without React and reused by the persistence layer.

In CartContext provider, the reducer's initial state is hydrated from useLocalStorage so a returning visitor sees their previous cart immediately. A useEffect mirrors every reducer transition back into storage through setStored, giving a single source of truth: the reducer owns in-memory state and the hook owns durability. Derived values like itemCount and subtotal are memoized with useMemo so they are only recomputed when state.items actually changes. The action creators are wrapped in useCallback and the whole context value in useMemo, which prevents consumers from re-rendering on unrelated parent updates.

The useCart consumer hook throws when used outside the provider, turning a subtle undefined-context bug into a loud, immediate error during development. A trade-off worth noting: localStorage is synchronous and size-limited, so this pattern suits carts and preferences but not large datasets, and prices should always be re-validated on the server at checkout since anything in localStorage is user-editable. For multi-device sync a backend store would eventually replace or back this local cache.

Share this code

Here's the card — post it anywhere.

Persist a React Shopping Cart to localStorage with a Context Provider — share card
Link copied