| name | typescript |
| description | Load for any TypeScript/React frontend work — dashboards and web UIs, project setup, components, data fetching, styling. |
The blessed way
TypeScript (strict) + React, npm for deps, shadcn/ui + Tailwind for UI. For full applications: Next.js, server-component-first. For a simple dashboard (read-mostly, polling), the sanctioned lighter path is Vite + React + TS (SPA) — chosen by the Architect and justified in the ADR. No any. Typed API boundaries.
Conventions
- Deps:
npm. package.json is the source of truth.
- TypeScript strict.
strict: true in tsconfig; no any (use unknown + narrowing). API responses are typed.
- App framework (pick one, per Architect):
- Next.js — blessed for real apps; server-component-first (client components only where interactivity needs them).
- Vite + React SPA — sanctioned light path for simple dashboards; documented in the ADR when chosen.
- UI: shadcn/ui + Tailwind. Don't hand-roll components that shadcn provides; don't introduce a second styling system.
- Components: function components + hooks; typed props; small and single-purpose (atomic). Early returns for loading/empty/error states.
- Data fetching in a dedicated layer (hooks/services), not inline in components; always render loading + error states.
- Style: paradigm-consistent, early returns, no dead state. Lint with ESLint.
Gotchas & AI failure modes
any everywhere. AI reaches for any to silence the compiler — defeats the point. Type the data.
- Everything client-side in Next. AI ignores server components and makes the whole tree
"use client". Server-component-first is the rule.
- Hand-rolled UI instead of shadcn — inconsistent, more code, more bugs.
- Missing loading/error/empty states — AI renders only the happy path; a live dashboard must handle "no data yet", "fetch failed", "stale".
- Stale-closure bugs in polling. A
setInterval capturing stale state/props is a classic AI miss; use refs or the functional updater, and clean up the interval.
- Untyped fetch responses —
res.json() as any propagates untyped data through the app.
- Bare global
JSX.Element — removed in @types/react v19. Annotate components with ReactElement (import type { ReactElement } from "react") or React.JSX.Element, never the bare global JSX.Element. (Caught live by tsc on the scaffold.)
How "done" is proven (tests)
tsc --noEmit (strict) passes + ESLint clean = the baseline.
- The UI acceptance criteria are demonstrable (the live list updates, per-item details show, live counts update) — evidence via screenshot/description or a component test.
- For logic-heavy pieces (e.g., the polling hook), a unit test (Vitest/RTL).
Out of scope (sanctioned paths or absent)
- State libraries (Redux/MobX/etc.): not default. React state/hooks first; a library only if the Architect justifies it in the ADR.
- CSS systems other than Tailwind: not blessed.
- The polling-vs-websockets decision: owned by the Architect per engagement (justified in the ADR), not hard-coded here. For modest update rates, polling is usually the simpler, defensible default.
Examples
Typed polling hook (the blessed shape — no stale closure, cleans up):
function useCounters(intervalMs = 2000): Record<string, number> | null {
const [counts, setCounts] = useState<Record<string, number> | null>(null);
useEffect(() => {
let alive = true;
const tick = async () => {
const res = await fetch("/counters");
const data = (await res.json()) as Record<string, number>;
if (alive) setCounts(data);
};
void tick();
const id = setInterval(tick, intervalMs);
return () => { alive = false; clearInterval(id); };
}, [intervalMs]);
return counts;
}