React + Next.js
Category: Tech Stack · Areas: web, ui
Description
Category
tech-stack
Areas
web, ui
Components
- UI Framework: React 19 — functional components and hooks only
- Meta-framework: Next.js 15 — App Router (not Pages Router)
- Component library: shadcn/ui (copied components, not npm dependency) + Radix UI primitives
- Styling: Tailwind CSS 4 — utility-first, no CSS-in-JS
- Forms: react-hook-form + @hookform/resolvers with Zod schemas
- Data tables: TanStack React Table 8 — headless, sortable, filterable, virtualizable
- Validation: Zod — shared schemas between frontend and backend
- E2E testing: Playwright
Constraints
- Use App Router (
app/directory) — not Pages Router (pages/) - Prefer React Server Components for data-heavy pages; use
"use client"only where interactivity is required - No class components — functional components with hooks only
- No
React.FCtype annotation — use plain function signatures with typed props - Forms must use react-hook-form with uncontrolled components — no
useStateper field - Validation schemas live in the shared package and are reused on frontend and backend
- shadcn/ui components are copied into the project — do not install as npm dependency
- Tailwind config extends the design system tokens (colors, spacing, typography)
- E2E tests use Playwright, not Cypress or Selenium
Drift Signals (anti-patterns to reject in review)
pages/directory for routing → must useapp/(App Router)class extends React.Component→ must use functional componentsReact.FCorReact.FunctionComponent→ use plain typed functionsuseStatefor every form field → must use react-hook-formstyled-components,emotion, orcss-modules→ use Tailwind utility classes@shadcn/uias npm dependency → must be copied componentscypressorselenium→ use PlaywrightgetServerSidePropsorgetStaticProps→ use Server Components or route handlers- Inline
fetchin components without error/loading states → use data fetching pattern with Suspense boundaries
When to use
React + Next.js frontend applications. Compose with typescript-bun for the
base TypeScript and Bun runtime concern. This concern adds React-specific UI
patterns, component library conventions, and E2E testing requirements.
ADR References
- ADR-010: Frontend validation architecture (Zod shared schemas)
- ADR-011: ERP component library and navigation (shadcn/ui + Radix + Tailwind)
Practices by activity
Agents working in any of these activities inherit the practices below via the bead’s context digest.
Requirements (Frame activity)
- User stories involving UI must specify which pages or components are affected
- Acceptance criteria for UI features must include Playwright E2E coverage
- Data-heavy views (tables, dashboards) must specify expected row counts for virtualization decisions
Design
- Use Next.js App Router: layouts in
app/layout.tsx, pages inapp/page.tsx, route groups with(groupName)/ - Default to React Server Components — add
"use client"only for interactive components (forms, modals, dropdowns, client state) - Component hierarchy: page (server) → layout (server) → interactive widget (client)
- shadcn/ui for UI primitives — copy components via
npx shadcn@latest add <component>, customize incomponents/ui/ - Radix UI for accessible headless primitives underlying shadcn
- Design tokens (colors, spacing, typography) in
tailwind.config.ts— components reference tokens, not raw values - Forms: one Zod schema per entity in
@apogee/shared, resolved via@hookform/resolvers/zod - Data tables: TanStack React Table with column definitions typed against shared schemas
- State management: server state via TanStack Query (when added), client state via Zustand (when added), form state via react-hook-form — no Redux
Implementation
- Component files:
ComponentName.tsxin PascalCase - Props: define inline or as
type Props = { ... }— nointerfacefor props, noReact.FC - Hooks:
useprefix, one file per hook inhooks/directory - Server Components: default export async functions, fetch data directly
- Client Components:
"use client"directive at top of file, minimize scope - Tailwind classes: use
cn()utility (from shadcn) for conditional classes — noclassnamesorclsxseparately - Forms pattern:
const form = useForm<SchemaType>({ resolver: zodResolver(schema) }); - Error boundaries: wrap route segments with
error.tsxfiles - Loading states: use
loading.tsxfiles or Suspense boundaries - Images: use
next/imagewith explicit width/height — no unoptimized<img>tags - Links: use
next/link— no<a>tags for internal navigation
Testing
- Unit tests:
bun:testfor component logic, hooks, and utilities - E2E tests: Playwright in
tests/e2e/directory - Playwright config: headless Chromium, 30s timeout, screenshots on failure
- Test naming:
feature-name.spec.ts - Use page object pattern for complex flows
- Run E2E:
bun run test:e2e(headless) orbun run test:e2e:headed(visible browser) - Do not mock API responses in E2E — test against real backend with seeded data
Quality Gates (pre-commit / CI)
bun run typecheck— tsc passes for web package (includes JSX type checking)bun run lint— Biome passes (includes JSX/TSX rules)bun test— unit tests passbun run test:e2e— Playwright E2E tests pass (CI only, requires running backend)- No
anyin component props or hook return types - No inline styles — use Tailwind classes
Accessibility
- All interactive elements must have accessible labels (aria-label, aria-labelledby, or visible text)
- shadcn/ui + Radix provide WCAG 2.1 AA keyboard and screen reader support by default — do not override with custom handlers that break accessibility
- Color contrast must meet WCAG AA (4.5:1 for normal text, 3:1 for large text)
- Forms must associate labels with inputs and display validation errors accessibly