React + Vite (single-page app)
Category: Tech Stack · Areas: ui
Description
Category
tech-stack
Areas
ui
Slot
frontend-framework
Boundary
This concern owns a client-rendered React single-page app built with Vite and
TypeScript whose production output is static files a backend serves: the Vite
project layout, strict TypeScript, React function components, client-side
routing, the dev-server proxy to the backend, the build output handed to the
backend, client environment exposure, the bundle budget, and component-level
testing. It is platform-neutral; any backend that can serve static files and an
index.html fallback can host it. It must not duplicate its neighbors:
react-nextjsis the otherfrontend-frameworkfiller and the shipped default. The slot is exclusive: select this concern throughconcerns.local.ymlwhen the product needs no server rendering, and Next.js when it does. Server Components, SSR, and route handlers arereact-nextjs’s.frontend-architectureowns the state, data-fetching, and rendering patterns (server state vs client state, colocation, async UI states). This concern fixes the rendering strategy to client-side rendering and names the libraries that express those patterns.ux-interaction-patternsowns neutral behavior; the selected library concern (ux-radixorpackaged-design-system) owns its component prescription;a11y-wcag-aaowns accessibility compliance;databricks-appkit-uiowns the Databricks component layer when selected.api-styleowns the wire contract the SPA consumes.- The backend and
deploy-targetown serving the build: the backend serves the output directory, answers unknown non-API paths withindex.html, and owns/api/*. On Databricks,databricks-appsstates those hosting requirements and builds the SPA through its rootpackage.jsonhook. typescript-bun, when it fillslanguage-runtime, owns the toolchain (Bun, Biome,bun:test); this concern’s toolchain defaults (npm or pnpm, Vitest) apply only when the backend uses another runtime.e2e-playwright(thee2e-frameworkslot) owns browser e2e against the running app; this concern owns component and hook tests.
Components
- UI framework: React 19, function components and hooks only
- Build tool and dev server: Vite with
@vitejs/plugin-react - Language: TypeScript in
strictmode, type-checked bytsc --noEmit(Vite strips types without checking them) - Routing: React Router as a client-side library (declarative or data mode), never its framework mode
- Server state: TanStack Query against the backend’s
/api/*routes (the pattern isfrontend-architecture’s) - Styling: Tailwind CSS 4 through
@tailwindcss/vite - Forms and validation: react-hook-form with Zod schemas
- Linting: ESLint with
eslint-plugin-react-hooks(Biome whentypescript-bungoverns the toolchain) - Component testing: Vitest with Testing Library and jsdom (
bun:testwhentypescript-bungoverns the toolchain); MSW for network boundaries - E2E testing: see
e2e-playwright - Layout: a
frontend/directory holdingindex.html,vite.config.ts,tsconfig.json, andsrc/(main.tsx,routes/,components/,hooks/,lib/), with a lockfile committed beside itspackage.json(which sits at the repo root instead when the host builds from there)
Constraints
Client-rendered only
- No SSR, React Server Components, or meta-framework (Next.js, Remix, React
Router framework mode, Astro). A product that needs server rendering selects
react-nextjsinstead. index.htmlis the single HTML entry. Routes use browser history (createBrowserRouter), not hash routing; deep links work because the backend returnsindex.htmlfor unknown non-API paths.
One origin with the backend
- API calls use relative
/api/...URLs. No hard-coded host, and no CORS configuration in production. - Development runs two processes: the backend on its local port and the Vite dev
server, whose
server.proxyforwards/apito the backend. build.outDiris the directory the backend serves (e.g.../backend/static), withemptyOutDir: true. Build output is generated, never committed.
Client environment exposure
- Only
VITE_-prefixed variables reach the browser, throughimport.meta.env.envPrefixis not widened, and no secret is ever placed in aVITE_variable; runtime configuration the client needs comes from an/api/*route.
TypeScript strict
strict: trueandnoUncheckedIndexedAccess: true; noanyin props or hook return types;tsc --noEmitruns as a gate becausevite builddoes not type-check.- No class components, and no
React.FC; props are plain typed function parameters.
Bundle budget
- Routes are code-split with
React.lazyand dynamicimport(). The TD records an initial-load JavaScript budget (HELIX default 250 KB gzipped, not a Vite or platform limit) and CI fails the build when it is exceeded.
Component tests
- Vitest (or
bun:testundertypescript-bun) with Testing Library covers components and hooks, querying by role and label rather than test IDs or class names. Network calls are mocked at the fetch boundary with MSW, never by mocking the query library.
Drift Signals (anti-patterns to reject in review)
next,@remix-run/*, or@react-router/dev(React Router framework mode) inpackage.json→ selectreact-nextjsif SSR is required; otherwise remove itprocess.env.Xin client code →import.meta.env.VITE_X- A secret, token, or key in a
VITE_variable → keep it on the backend fetch("http://localhost:8000/api/...")or any absolute API host → relative/api/...plus the dev-server proxy- CORS middleware added so the SPA can reach its own backend → serve both from one origin
HashRouter→ browser history routing with the backend’sindex.htmlfallback- A committed
dist/orstatic/build → build in CI or on the platform - No
tsc --noEmitin the gates → add it;vite builddoes not type-check class extends React.ComponentorReact.FC→ plain typed function components- Jest, Enzyme, or snapshot-only component tests → Vitest (or
bun:testundertypescript-bun) + Testing Library assertions on roles and text - Cypress or Selenium →
e2e-playwright - One eagerly loaded bundle over budget → route-level
React.lazysplitting - Re-prescribing shadcn/ui or Radix here → defer to
ux-radix
When to use
A React single-page app served as static files by a separate backend (Python,
Go, JVM, or a Node API) or by a static host, where no page needs server
rendering for SEO or first-paint data. Selection signal: the backend is not
a Node/Next.js server, or the operator wants a plain SPA; select it by setting
frontend-framework: react-vite under defaults: in
docs/helix/01-frame/concerns.local.yml.
Compose with the backend’s language-runtime concern, ux-interaction-patterns,
and the selected component owner,
frontend-architecture, a11y-wcag-aa, and e2e-playwright; on Databricks,
with databricks-apps and optionally databricks-appkit-ui.
Do not select it for products that need SSR, React Server Components, or
SEO-indexed dynamic pages (use react-nextjs), or for content and documentation
sites (use hugo-hextra).
Artifact Impact
Selecting this concern requires these artifacts to change (a selected concern absent from them is drift):
- TD: Vite layout, dev proxy of
/api,build.outDirhanded to the backend,VITE_env rule, initial-load bundle budget - IMPLEMENTATION_PLAN: frontend scaffold, build wiring into the backend’s static directory,
tsc --noEmitand bundle-budget gates - DESIGN_SYSTEM: Tailwind 4 tokens the components reference (component-library conventions live with the selected component owner)
- TEST_PLAN: Vitest (or
bun:testundertypescript-bun) + Testing Library component tests; browser e2e pere2e-playwright
Component ownership and package integration
Framework selection does not select a component library. Apply the neutral ux-interaction-patterns baseline and the project’s chosen component owner. References above to ux-radix describe the Radix/shadcn choice only; they do not require that choice. With packaged-design-system, use its documented components and tokens; record Tailwind defaults as overridden where they prescribe independent tokens or widget styling. With typescript-bun, run applicable supplied checks alongside the selected linter, or define project checks when the package provides none. Installed-version package documentation controls integration.
Practices by activity
Agents working in any of these activities inherit the practices below through runtime work context, such as a DDx bead context digest.
Requirements (Frame activity)
- Confirm no page needs server rendering for SEO or first-paint data; if one does, the slot filler is
react-nextjs, not this concern. - Name the backend that serves the build and confirm it can serve a static directory with an
index.htmlfallback on the same origin as its/api/*routes. - Set
build.outDirto the backend’s static directory withemptyOutDir: true; the backend owns serving it. - Configure
server.proxyto forward/apito the backend’s local port; all API calls use relative/api/...URLs. - State the initial-load JavaScript budget if the HELIX default (250 KB gzipped) does not fit the product.
- User stories involving UI must specify which routes or components are affected.
- Acceptance criteria for UI features must include browser e2e coverage per
e2e-playwright.
Design
- Keep the SPA in a
frontend/directory withvite.config.ts,tsconfig.json,index.html, andsrc/; itspackage.jsonsits where the host’s build installs from (frontend/, or the repo root). - Define routes with React Router’s
createBrowserRouteras a client-side library; lazy-load each route module. - Fetch server state with TanStack Query; keep client-only UI state in components (patterns per
frontend-architecture). - Validate forms with react-hook-form and Zod; mirror, never replace, the backend’s validation.
- Reference design tokens through Tailwind 4 theme variables; UI behavior comes from
ux-interaction-patterns; the selected component owner supplies primitives (Radix/shadcn, AppKit, or packaged-design-system). - Backend configuration follows its
language-runtimeconcern’s central-config rule; the SPA never reads it directly. - Expose to the client only
VITE_-prefixed variables that are safe to publish; serve runtime configuration from an/api/*route.
Implementation
- Write function components with typed props (
type Props = { ... }); no class components, noReact.FC. - Component files are
ComponentName.tsxin PascalCase; hooks use theuseprefix, one per file inhooks/. - Read client configuration only from
import.meta.env.VITE_*; never referenceprocess.envin client code. - Wrap each lazy route in a Suspense boundary and an error boundary, so every route renders loading and error states.
- Keep
tsconfig.jsonatstrict: truewithnoUncheckedIndexedAccess: true; noanyin props or hook return types. - Commit the package manager’s lockfile; never commit the build output.
- Use a
cn()helper for conditional Tailwind classes; no inline styles.
Testing / Verification
- Component and hook tests use Vitest + Testing Library in jsdom (
bun:testundertypescript-bun), querying by role, label, and text. - Mock the network at the fetch boundary with MSW; do not mock TanStack Query or the router.
- Test each route’s loading, empty, error, and success states.
- Verify a production build served by the real backend: a deep link to a client route loads the SPA, and
/api/*reaches the backend. - Browser e2e of core flows runs per
e2e-playwrightagainst the running backend, not the Vite dev server alone.
Quality Gates
tsc --noEmitpasses for the frontend.- The lint gate passes (Biome under
typescript-bun, otherwise ESLint with the React hooks rules). - The ESLint config sets the complexity, function-length, and depth ceilings and a file-size cap per
code-shape-ceilings; no ceiling is raised and noeslint-disableis added for them. - The component test suite passes (
vitest run, orbun testundertypescript-bun). vite buildsucceeds and the initial-load JavaScript stays within the TD budget.- No
VITE_variable holds a secret, and no client code referencesprocess.env. - No absolute API host in client code; all API calls are relative
/api/....
Component ownership and package integration
Framework selection does not select a component library. Apply the neutral ux-interaction-patterns baseline and the project’s chosen component owner. References above to ux-radix describe the Radix/shadcn choice only; they do not require that choice. With packaged-design-system, use its documented components and tokens; record Tailwind defaults as overridden where they prescribe independent tokens or widget styling. With typescript-bun, run applicable supplied checks alongside the selected linter, or define project checks when the package provides none. Installed-version package documentation controls integration.
Innsigle seal: model-primary by HELIX
The signature covers the markdown source of this page, not these HTML bytes. This page quotes that seal; verify it against the source file.
- Composition
- model-primary
- Issuer
- HELIX
helix - Signing key
ed25519:b0865d76d834a52c48506414d16f4e5a(build key)- Signed source
concerns/react-vite.md- Signed
- 2026-10-22T18:09:54Z
- Content digest
sha256:52c57a89…ef7cb984
This build key is endorsed by the human key for build signing; the signature is not a detector and not a truth guarantee.
Raw attestation JSON
{
"payload": {
"innsigle": "1",
"type": "https://innsigle.dev/claim/colophon/v1",
"issued_at": "2026-10-22T18:09:54Z",
"issuer": {
"id": "helix",
"name": "HELIX",
"key_id": "ed25519:b0865d76d834a52c48506414d16f4e5a",
"key_url": "https://documentdrivendx.github.io/helix/.well-known/innsigle/keys.json"
},
"subjects": [
{
"uri": "https://documentdrivendx.github.io/helix/concerns/react-vite/",
"digest": {
"alg": "sha256",
"value": "52c57a896c4f3bf444ff5c10482340fa9a2577913284a64aa2922ec1ef7cb984"
}
}
],
"colophon": {
"schema_version": "1",
"composition": "model-primary",
"ingredients": [
{
"kind": "model",
"name": "Claude",
"role": "draft"
},
{
"kind": "tool",
"name": "sloptimizer",
"role": "rewrite"
},
{
"kind": "human",
"name": "operator",
"role": "structure-edit"
}
],
"notes": null
}
},
"payload_encoding": "json",
"signatures": [
{
"key_id": "ed25519:b0865d76d834a52c48506414d16f4e5a",
"alg": "ed25519",
"sig": "7huPE51ypJ0hzngyQCJUmuCznKOOwHs5P-c09j5yho7-KrYxGjND_npBX25M5wvpa7nbTKX2PE3Kllrk3dftCw",
"signed_at": "2026-10-22T18:09:54Z"
}
]
}