Skip to content

08 — Checkout Customization Challenge

Context: This is an actual coding challenge from the Glomopay interview process. Build a checkout customization page with a form, live preview, mock API, dirty tracking, save/reset, and validation.

Requirements

  1. Two-column layout: form on the left, live preview on the right
  2. Form fields: button text (text input) and background color (hex color input + picker)
  3. Live preview updates as the user types (no submit needed for preview)
  4. Mock API: fetchCustomisation() loads initial values, saveCustomisation() persists changes
  5. Dirty tracking: Save/Reset buttons only enabled when form differs from saved state
  6. Validation: button text required (min 3 chars), background color must be valid #rrggbb
  7. Save disabled when form is invalid or clean
  8. Reset reverts to last saved values (no API call)

Architecture

CustomisationPage          ← layout orchestrator, calls useCustomisation hook
├── CustomisationForm      ← form inputs, error display, button state
└── CheckoutPreview        ← pure presentational, receives buttonText + backgroundColor

Three components + one custom hook. No Context — the tree is flat enough that props work cleanly.

Why useState over useReducer

The practice-checkout project already demonstrates useReducer + Context for a multi-step checkout flow. This project intentionally uses useState to show when the simpler tool is the right choice:

  • Two fields — the state shape is flat, not a complex object with many transitions
  • No complex transitions — no discriminated union of action types needed
  • Derived state handles the complexity — isDirty, errors, and isValid are computed each render, not stored

Key Patterns

Derived State (not stored)

tsx
// Computed every render — never out of sync
const isDirty =
  formValues.buttonText !== savedValues.buttonText ||
  formValues.backgroundColor !== savedValues.backgroundColor;

const errors = validate(formValues);
const isValid = !hasErrors(errors);

Why: Storing derived state creates sync bugs. If isDirty were in useState, you'd need to update it every time formValues or savedValues change — easy to miss one.

Form vs Saved Diffing

The hook maintains two pieces of state:

  • formValues — what the user sees in the form right now
  • savedValues — what the API last returned

This creates a clean mental model:

  • Reset = copy savedValues into formValues
  • Save = send formValues to API, update both from response
  • isDirty = formValues !== savedValues (field-by-field comparison)

Pure Validation Functions

ts
export function validateButtonText(value: string): string | undefined {
  if (!value.trim()) return 'Button text is required';
  if (value.trim().length < 3) return 'Button text must be at least 3 characters';
  return undefined;
}

Validation is extracted as pure functions — no hooks, no state, no side effects. This makes them trivially testable and reusable.

Presentational vs Container Split

  • CheckoutPreview is purely presentational — receives props, renders UI, no state
  • CustomisationForm is also presentational — receives values, errors, and callbacks as props
  • CustomisationPage is the container — calls the hook, passes data down

This follows the classic container/presentational pattern. The form doesn't know about the API or the hook. It just renders what it's given and calls back when things change.

What the Interviewer Is Testing

AreaWhat they want to see
React + TypeScriptProper typing of props, state, and API responses
State managementChoosing the right tool (useState vs useReducer), derived state, avoiding sync bugs
Component architectureClean separation of concerns, testable components
Form handlingValidation, dirty tracking, error display, button state
TestingBehavior-driven tests, async handling, user interaction simulation
SOLIDSingle responsibility (hook vs components vs validation), open/closed (validation is extensible)

Running the Practice Project

bash
cd glomopay-prep/practice-customization
npm install
npm run dev    # Start dev server
npm test       # Run tests

Frontend interview preparation reference.