
Apple Accessories
An e-commerce clone. Mostly about checkout, state, and the decisions behind them.
THE EXPERIMENT
Built an e-commerce clone with cart persistence, product variants, and live Stripe checkout.
The project started as an experiment in state management. XState kept checkout decisions explicit, but the more useful lesson was learning where state boundaries mattered, and where they did not.
THE PROBLEM
Checkout state grows quietly.
Selected colours, quantities, hydration, persistence, payment readiness. The difficulty was not making the cart work. It was deciding where those concerns should live once they started touching the same state.
WHAT I DECIDED
I chose XState v5 after using Redux patterns elsewhere and wanting to explore more explicit event-driven flows.
Five typed events handled cart mutations and hydration: ADD_TO_CART, DELETE_FROM_CART, CHANGE_PRODUCT_COUNT, HYDRATE_CART, CLEAR_CART.
Cart state used a nested structure: { [productId]: { total, [colorKey]: count } }.
The goal was keeping bulk operations, per-colour queries, and persistence inside one predictable flow instead of scattered updates.
THE EDGE CASE THAT TAUGHT ME
Persisted data survives longer than assumptions.
Old localStorage schemas could leave invalid product IDs behind. Validation ran during hydration and dropped entries before cart state rendered.
Private browsing could fail on localStorage.setItem(). Storage availability was checked before persistence happened at all.
Neither issue appeared in the happy path. Both break real users.
IF I REBUILT IT TODAY
I would probably reach for Zustand first.
The app was not complex enough to justify a full state machine, even though XState made transitions explicit. I would keep the boundaries around persistence and hydration, but choose a lighter layer for managing state.
I would also move validation closer to the product schema and add tests around hydration, where failures stay silent.
INSTRUMENTS
- Daily drivers
- Next.js 13 · React 18 · TypeScript
- State
- XState v5 · createActorContext
- Payment
- Stripe · @stripe/stripe-js
- Styling
- Tailwind · React Slick