javascript 45 lines · 7 steps

A finite state machine for checkout flow

A lookup table of states and actions drives valid transitions, emitting events on every move.

Explained by highlit
1const transitions = {
2 cart: { checkout: 'shipping' },
3 shipping: { submitAddress: 'payment', back: 'cart' },
4 payment: { submitPayment: 'review', back: 'shipping' },
5 review: { confirm: 'processing', back: 'payment' },
6 processing: { success: 'complete', failure: 'payment' },
7 complete: {},
8};
9 
10class CheckoutMachine extends EventTarget {
11 constructor(state = 'cart', context = {}) {
12 super();
13 this.state = state;
14 this.context = context;
15 }
16 
17 can(action) {
18 return action in (transitions[this.state] ?? {});
19 }
20 
21 dispatch(action, payload = {}) {
22 const next = transitions[this.state]?.[action];
23 if (!next) {
24 throw new Error(`Invalid action "${action}" from state "${this.state}"`);
25 }
26 
27 const from = this.state;
28 this.context = { ...this.context, ...payload };
29 this.state = next;
30 
31 this.dispatchEvent(
32 new CustomEvent('transition', {
33 detail: { from, to: next, action, context: this.context },
34 })
35 );
36 
37 return this.state;
38 }
39 
40 get isTerminal() {
41 return Object.keys(transitions[this.state] ?? {}).length === 0;
42 }
43}
44 
45export { CheckoutMachine, transitions };
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Encoding allowed transitions as data keeps the state logic declarative and easy to audit.
  2. 2Looking up the next state before mutating lets you reject invalid actions cleanly.
  3. 3Extending EventTarget gives observers a standard way to react to each transition.

Related explainers

typescript
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';

Wiring validation and Swagger docs in NestJS

validation openapi decorators
Intermediate 8 steps
javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js';
 
export interface NormalizedPhone {
  e164: string;

Normalizing phone numbers to E.164 in TypeScript

validation normalization error-handling
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

A finite state machine for checkout flow — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code