javascript 127 lines · 3 tabs

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.


Related snips

javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
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
javascript
// Get canvas and context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 1. Basic shapes
// Rectangle (filled)

Canvas API for graphics and animations

canvas javascript graphics
by Alex Chang 1 tab

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