Skip to main content

Decisions

ADR 0007 — UI stack: React Router, TanStack Query, Tailwind, Radix

Architecture decision records — why it was built this way.

docs/adr/0007-ui-stack.md
On this page

Status: Accepted Date: 2026-07-26 Wave: W6

Context

The v1 UI was 2,640 lines of hand-rolled React with:

  • no router — the active tab lived in useState, so there were no deep links, browser back did nothing, and a refresh dumped you on the dashboard,
  • no data layer — every view hand-wrote fetch, loading and error handling, with a refreshTick counter threaded through props to force refetches,
  • no design system — a single hand-written stylesheet, one theme,
  • no tests at all.

The product requirement is a UI that is engaging, descriptive and easy to use. That is not reachable by patching this.

Decision

Rebuild on: React 18 + TypeScript (strict) + Vite, React Router for real URLs, TanStack Query for server state, Tailwind with design tokens for styling, and Radix primitives for accessible dialogs, menus, tabs and tooltips. Tested with Vitest + Testing Library + MSW at the component layer and Playwright for journeys.

The built SPA continues to be embedded into the Go binary — one artifact.

Alternatives considered

Alternative Why rejected
Incremental polish of v1 Keeps the structural limits (no routing, no caching) that cause the UX problems
Next.js SSR and a Node runtime buy nothing for an embedded SPA served by a Go binary; adds deployment weight
Component kit (MUI, Chakra, Ant) Heavy, opinionated visual identity that is hard to make feel like an operations console; Radix gives behaviour and a11y without imposing looks
CSS Modules / vanilla-extract Fine choices; Tailwind wins on iteration speed and on making a token system enforceable in review
Redux / Zustand for server state The hard problem here is server-state caching and invalidation, which TanStack Query solves directly. Local UI state stays in components

Consequences

  • Easier: deep links work, back works, refresh preserves state. Caching, retry and invalidation-on-run-completion are configured once, not per view. Accessibility comes from Radix rather than from remembering ARIA attributes.
  • Harder: more dependencies to keep current. Mitigated by Dependabot and the licence scan gate.
  • Design tokens must be defined before views are built, otherwise Tailwind becomes inconsistent utility soup. Tokens are W6-T03, ahead of every view task.
  • MSW mocks at the network layer, so component tests exercise the real fetch path and catch serialisation mistakes that module stubbing would hide.

The incident field notes

One real Kubernetes failure a week — the symptom, the commands that found it, and the fix. Written from actual lab runs, not from memory.

You'll get the Kubernetes Incident Response Field Guide, plus occasional emails about new scenarios, posts and paid offerings such as courses and workshops. Unsubscribe any time.