Prescriptive React + TS Greenfield Scaffolding Defaults
best-practicesfrontend-engineeringreact-typescript
--- name: frontend-engineering description: >- Prescriptive engineering choices for generating new (greenfield) React + TypeScript projects. Use when scaffolding or generating a new React/TS app, component library, or frontend feature set to make consistent, defensible decisions across architecture, type safety, state, data fetching, styling, routing, forms, accessibility, performance, testing, security, observability, and delivery. Provides named default tools with rationale and deviation guidance. metadata: stack: React + TypeScript + Vite (SPA) package_manager: npm repo_shape: single package purpose: greenfield code generation --- # Frontend Engineering Choices (React + TypeScript) Prescriptive defaults for generating **new, single-package React + TypeScript SPAs built with Vite**. Apply these when scaffolding a new project. Deviate only when a project's requirements explicitly conflict — and say why when you do. **Baseline stack:** React + TypeScript, Vite (SWC), npm, Node 24, single package, client-rendered SPA (no SSR/SSG). Static-deployable to any Linux host. ## A. Foundation & Tooling - Scaffold with Vite's `react-swc-ts` template. Keep its default `tsconfig` as-is — do **not** add custom strictness overrides. - Build with Vite + `@vitejs/plugin-react-swc`. Production build is `vite build` → static assets. - Commit `package-lock.json`; use `npm ci` in CI. Keep only Vite's default scripts (`dev`, `build`, `preview`) at scaffold time; other scripts arrive with their own aspects (lint, typecheck, test). - Target **Node 24**. Pin via `.nvmrc` and `engines.node` in `package.json`. ## B. Language & Type Safety - Use `interface` for object/prop/public shapes; use `type` for unions, intersections, and mapped types. Model variants as **discriminated unions**. - Never use `any` — prefer `unknown` and narrow. Use `satisfies` for typed literals. - Inherit Vite's default `strict: true` config. Add no custom compiler flags. - Parse **all untrusted external data with Zod** at the boundary (API responses, `localStorage`, URL params, form input). Parse once at the edge, then trust the inferred type inward. - Validate required `VITE_*` env vars once at startup with a Zod schema; export a typed `env` object. App code imports `env`, never `import.meta.env` directly. - Zod-inferred types (`z.infer`) are the canonical API types. Do not hand-maintain parallel interfaces. ## C. Architecture & Project Structure - Use a **feature-based** layout: `src/features/<feature>/` holds that feature's components, hooks, api, and types. Put shared cross-feature code in top-level `src/components/`, `src/lib/`, `src/hooks/`. - Features must not import each other's internals. Promote shared code up instead. - Two component layers: reusable presentational primitives in `src/components/ui` (no business logic), and feature components that compose primitives + logic. Push logic into hooks — do **not** use a container/presentational split. - Alias `@/` → `src/` (tsconfig paths + Vite resolve). **Avoid barrel `index.ts` files** (tree-shaking and circular-import problems); import directly. - Components render; custom hooks hold logic and data-fetching; pure helpers live in `lib/`. Wrap feature data access in `useX()` hooks. - Code-split at the **route level** with `React.lazy` + `Suspense`. Don't split below routes except for genuinely heavy components (charts, editors). - Add a dependency only when it solves a real, non-trivial problem, is well-maintained, and has reasonable bundle cost. Prefer platform APIs and the tools already prescribed here. ## D. Component Design & UI - Function components only. Type props via `interface`. - **Composition over configuration**: prefer `children` and sub-components over many boolean/config props. Avoid prop explosion. - When wrapping native elements, use `React.ComponentProps<'button'>` etc. and spread rest props. - Build primitives on **headless, accessible** behavior libraries (Radix) and style them in-repo. - Use **shadcn/ui** (Radix + Tailwind) as the component system. Copy components into `src/components/ui` and own them in-repo. - Prefer **uncontrolled** with sensible defaults for simple inputs; expose a controlled API (`value` + `onChange`) when the parent needs the value; support both via `value`/`defaultValue`. Forms go through Section I. - Use the **compound-component** pattern for multi-part components, backed by a small local Context. Keep context scoped — it is not a substitute for global state (Section E). ## E. State Management Default to local state and escalate deliberately: ``` Local (useState/useReducer) → component-specific UI state Lifted props → shared between 2-3 siblings URL search params → shareable/navigable state (filters, tabs, paging) Server state (TanStack Query)→ remote data with caching Zustand (deferred) → cross-tree client state, only once complex ``` - Use **TanStack Query** for all server state (fetch/cache/revalidate/mutations). Never duplicate server data into client state. - **Zustand** is the chosen global-state library but is **not installed by default**. Add it only later, when the app grows complex and local state is insufficient. Keep stores small and sliced by concern. - Treat the **URL as the source of truth** for shareable state via the router's search-param APIs (Section H). Validate params with Zod. - Use **React Hook Form + Zod resolver** for non-trivial forms (Section I). - Compute derived values during render. Use `useMemo`/`useCallback` only for measured expensive work or referential stability — not by default. React Compiler is not prescribed yet. ## F. Data Layer - Fetch with **TanStack Query over native `fetch`** (no axios). Use a thin typed client and expose data via feature `useX()` hooks. Structure query keys as arrays (`['feature', id]`). - Default to **REST via `fetch`** with a thin wrapper (base URL, headers, JSON, error normalization). Validate every response with Zod at the boundary; derive types from `z.infer`. Use GraphQL/tRPC only when the backend uses them. - Use TanStack Query defaults with a sensible per-query `staleTime`. Mutations invalidate affected keys. Use optimistic updates for high-frequency, low-risk actions; otherwise invalidate-and-refetch. - Every data view handles **loading, error, empty, and success** explicitly: loading via Suspense/`isPending` with skeletons; errors via error boundaries (Section O) plus inline retry; empty states are designed, not blank. - Use `useInfiniteQuery` for infinite lists and page-based queries for pagination. Prefer cursor-based pagination when the API supports it. Virtualize long lists (Section K). No streaming/SSR. ## G. Styling & Design - Use **Tailwind CSS v4**, utility-first. Compose variants with `cva` + `tailwind-merge` + a `cn()` helper (the shadcn convention). No CSS-in-JS. Use plain CSS only for the global reset/base layer. - Define design tokens as **CSS variables** consumed by the Tailwind theme, with semantic names (`background`, `foreground`, `primary`, `muted`). **Do not add dark mode by default** — light theme only. - Respect typographic hierarchy: one `h1` per page, no skipped heading levels, never use heading styles for non-heading content. - Design **mobile-first** using Tailwind's default breakpoints (`sm`/`md`/`lg`/`xl`/`2xl`); avoid custom breakpoints unless needed; use container queries where appropriate. Stay on the spacing scale — no arbitrary pixel values. Test at **320 / 768 / 1024 / 1440px**. - Animate with CSS transitions / `tailwindcss-animate` utilities. No Framer Motion. Respect `prefers-reduced-motion`. - Use **lucide-react** for icons. Import SVGs as components via Vite. Optimize raster assets (Section K). Self-host fonts (`@fontsource`/local); no render-blocking external font CSS. ### Avoid the AI aesthetic Steer away from the generic AI-generated look: - No default purple/indigo palette — use the project's real palette. - No excessive gradients, no maximal rounding (`rounded-2xl` on everything), no shadow-heavy layering, no oversized uniform padding. - No stock card grids or generic hero sections — prefer content-first, purpose-driven layouts. - Use **realistic placeholder content**, not lorem ipsum — it reveals real length, wrapping, and overflow problems. ## H. Routing & Navigation - Use **React Router v7** (declarative/library mode). Define routes centrally in a typed route config. - Group route definitions by feature; lazy-load each route's page component with `React.lazy`. A central router module composes feature routes. - Use nested layout routes for shared shells (nav/sidebar). Gate auth with a `ProtectedRoute` wrapper/layout that checks session and redirects to login, preserving the intended destination. - Load data with **TanStack Query in components/hooks, not router loaders** (single data layer). Optionally prefetch on navigation intent (hover/focus). ## I. Forms & Validation - Use **React Hook Form** for all non-trivial forms. Plain `useState` is fine for trivial single-input cases (e.g. a search box). Integrate with shadcn/ui `Form` components. - Use **Zod** schemas as the single source of truth via `@hookform/resolvers` `zodResolver`. The same schema drives types (`z.infer`), client validation, and boundary parsing. No separate validation logic. - Accessible forms: associate labels via `htmlFor`/`id`; link errors with `aria-describedby` + `aria-invalid`; validate on submit and revalidate on change after the first error; focus the first invalid field on submit; show inline error text. shadcn/ui `Form` provides most of this wiring. ## J. Accessibility (WCAG 2.1 AA) - Use semantic elements first (`button`, `nav`, `main`, ordered headings). Use ARIA only to fill gaps, never to replace semantics. All interactive elements are real controls — no clickable `div`s. Radix primitives give correct roles/ARIA. - Everything must be keyboard-operable with visible focus rings (never remove an outline without a replacement). Trap focus in dialogs/menus (Radix handles it), return focus to the trigger on close, and provide a skip-to-content link. - Meet WCAG 2.1 AA contrast (4.5:1 text, 3:1 large text/UI). Never rely on color alone — pair with icon or text. Respect `prefers-reduced-motion`. - Check a11y with `eslint-plugin-jsx-a11y` (static) and axe (`@axe-core/react` in dev, axe assertions in tests). Do a manual keyboard + screen-reader smoke test of key flows. ## K. Performance - Analyze bundles with `rollup-plugin-visualizer` on demand. Keep initial route JS lean via route-level splitting. Prefer lighter deps. Treat budgets as soft guidance, not hard CI gates. - Control re-renders through component structure and stable props first; memoize only when measured. Virtualize long lists (`@tanstack/react-virtual`) beyond ~100 rows. Avoid inline object/array literals in hot paths. - Targets: **LCP < 2.5s, INP < 200ms, CLS < 0.1**. Reserve space for async content, prioritize above-the-fold, keep the main thread free. Measure with the `web-vitals` lib. - Optimize assets: responsive images with explicit `width`/`height`; lazy-load below-the-fold (`loading="lazy"`); modern formats (WebP/AVIF); self-hosted fonts with `font-display: swap` and a preload for the critical font. Vite hashes assets. - Loading strategy: route-level lazy + `Suspense` with skeletons; prefetch on navigation intent; `<link rel="preload">` for critical assets. ## L. Testing - Use **Vitest** (native Vite integration). Test behavior, not implementation. Unit-test pure logic (`lib/`, hooks, schemas). Colocate tests as `*.test.ts(x)` next to source. - Component/integration tests use **React Testing Library** + `user-event`, DOM via **jsdom**. Query by role/label. Integration tests over feature flows are the primary value. - **Playwright** is the chosen E2E tool but is **not installed by default**. Defer it until a project warrants E2E, then keep tests minimal (a few high-value smoke paths). - No dedicated visual-regression tooling by default. Add accessibility assertions via jest-axe/axe inside RTL tests for key components. - Mock the network at the boundary with **MSW** for component/integration tests. Use typed test-data factories that reuse Zod schemas so fixtures stay valid. - No hard coverage %. Prioritize business logic, hooks, forms/validation, and critical flows. Don't test trivial markup or third-party behavior. Use Vitest `--coverage` (v8) for visibility, not as a gate. ## M. Quality, Linting & Formatting - Use **ESLint** (flat config): Vite's default TS-ESLint setup plus `eslint-plugin-react-hooks`, `eslint-plugin-react-refresh`, and `eslint-plugin-jsx-a11y`. Stay close to recommended presets; no heavy custom rules. - Use **Prettier** with `prettier-plugin-tailwindcss` for class sorting. ESLint owns code-quality, Prettier owns formatting (`eslint-config-prettier` removes overlap). - **No pre-commit hooks.** Do not add Husky, lint-staged, or any git hooks. Rely on the editor and manual checks. - Type-check with `tsc --noEmit` as a `typecheck` script. The standard local check sequence is **lint → typecheck → test → build** (run manually; CI is off by default — this is what a CI job would run if one were added). ## N. Security - Rely on React's default escaping. Avoid `dangerouslySetInnerHTML`; if unavoidable, sanitize with **DOMPurify**. Never build HTML from strings. Validate all external data with Zod. Avoid `eval` and dynamic code. - Ship a strict **Content Security Policy** plus standard security headers (`X-Content-Type-Options`, `Referrer-Policy`, HSTS), set at the static host/CDN (Section R). - Prefer **httpOnly, Secure, SameSite cookies** (set by the backend) over `localStorage` for auth tokens. If a token must live in JS, keep it in memory only. Never store secrets or PII in `localStorage`. - Run `npm audit` for visibility. Keep dependencies current and minimal. Pin via the lockfile. Dependabot/renovate is optional. - `VITE_*` vars are **public by definition** — never put secrets in them. Real secrets stay server-side. Gitignore `.env` and commit a `.env.example`. ## O. Error Handling & Observability - Use **react-error-boundary** at the app root and around lazy routes and risky subtrees, with a friendly fallback offering reset/retry. TanStack Query handles data errors inline; boundaries catch render errors. - **Sentry** is the chosen error-reporting tool but is **not installed by default**. Add it when production monitoring is needed. Until then, log to console in dev with no noisy prod logging. - No analytics by default. When needed, prefer lightweight, privacy-friendly options (Plausible/Umami) over heavy trackers. - Measure Core Web Vitals locally with `web-vitals`. Report only if analytics/Sentry are enabled. No dedicated RUM service by default. ## P. Internationalization & Content - **i18n is off by default** — build single-locale (English) and defer install. `react-i18next` is the chosen tool when needed. Even without i18n, don't bury hardcoded user-facing strings in ways that block later extraction. - Use native `Intl` APIs (`Intl.DateTimeFormat`, `Intl.NumberFormat`) for date/number/currency formatting even single-locale. No heavy date libs; use `date-fns` only if date math is needed. - No RTL support by default (single LTR locale). Use logical CSS / Tailwind logical utilities where trivial so RTL isn't blocked later. ## Q. SEO & Metadata - `react-helmet-async` is the chosen head-management tool but is **deferred**. Rely on a good static `<head>` in `index.html` (title, description, viewport, charset). Add it only when per-route meta is actually needed. - Add Open Graph + Twitter Card meta in `index.html` for link previews when relevant. Use JSON-LD only when the app is content/SEO-relevant. - **Sitemap/robots are OFF by default.** Do not scaffold `sitemap.xml` or `robots.txt`. Add them only for an explicitly public, crawlable site. ## R. DX, CI/CD & Delivery - Use plain **npm scripts** (no task runner): `dev`, `build`, `preview`, plus `lint`, `format`, `typecheck`, `test` as their sections introduce them. No aggregate `check` script. - **No CI by default.** Do not scaffold GitHub Actions or workflow files. Add CI only when a project explicitly needs it. - Deployment is **host-agnostic** — no default host. Every SPA builds to static assets deployable on any Linux host. Configure SPA fallback (all routes → `index.html`) and the Section N security headers/CSP at the host. - Manage env with Vite mode files (`.env`, `.env.production`, `.env.local`). Only `VITE_*` is exposed, validated via Section B. Commit `.env.example`. No secrets client-side. - Ship a solid **README** (setup, scripts, structure, decisions). **No Storybook by default.** Lightweight ADRs are optional for notable decisions. - No versioning/changelog ceremony for app projects (continuous deploy). Use changesets/semver only for a publishable library. Rely on git history. ## S. Conventions & Collaboration - Naming: components in PascalCase files (`UserCard.tsx`); hooks `useX.ts` camelCase; non-component modules camelCase (`formatDate.ts`); types/interfaces PascalCase; constants `UPPER_SNAKE`. One component per file (barrel-free). Feature folders kebab-case (`user-profile/`). - Git: trunk-based with short-lived feature branches off `main`. Use **Conventional Commits** (`feat:`, `fix:`, `chore:`…) as a recommended convention (not hook-enforced). Branches: `feature/…`, `fix/…`. - PRs: small and focused with clear descriptions. Self-review that lint/typecheck/test pass locally. No CODEOWNERS by default. - Editor: ship `.editorconfig` and `.vscode/extensions.json` recommending ESLint, Prettier, and Tailwind CSS IntelliSense; `.vscode/settings.json` for Prettier format-on-save.
0 likes0 comments
Want to like, comment or save this prompt?
Sign up free to interact, create and organise your own AI prompts.
Get Started Free