Shopping Cart Checkout
Problem
A checkout flow needs replacement cart updates, guarded advancement, and durable committed state.
Solution
Use reducers for cart context, a guard for checkout admission, and a subscription for persistence.
ts
import { defineMachine } from '@vielzeug/clockwork';
type Item = { id: string; price: number; quantity: number };
type Event = { item: Item; type: 'ADD' } | { type: 'CHECKOUT' } | { address: string; type: 'SHIPPING' };
const checkout = defineMachine<{ address: string; items: Item[] }, Event>()({
context: { address: '', items: [] },
initial: 'shopping',
states: {
shopping: {
on: {
ADD: { reduce: ({ context, event }) => ({ ...context, items: [...context.items, event.item] }), target: 'shopping' },
CHECKOUT: { guard: ({ context }) => context.items.length > 0, target: 'shipping' },
},
},
shipping: {
on: {
SHIPPING: {
effects: [({ context }) => console.log('shipping address committed:', context.address)],
reduce: ({ context, event }) => ({ ...context, address: event.address }),
target: 'payment',
},
},
},
payment: {},
},
});
const actor = checkout.createActor();
const stop = actor.subscribe((snapshot) => localStorage.setItem('checkout', JSON.stringify(snapshot)));
actor.send({ item: { id: 'book', price: 29, quantity: 1 }, type: 'ADD' });
actor.send({ type: 'CHECKOUT' });
console.log(actor.snapshot.state); // 'shipping'
stop();
actor.dispose();Pitfalls
- Do not use client cart data for price or payment authorization; validate on the server.
- An effect observes committed context and should not mutate it.