Web Frontend
Overview
A frontend is five things: a rendering strategy, server state, client state,
a styling system, and a build/deploy pipeline. Most React problems in production
trace to two root causes: conflating server state with client state (which produces the
whole family of useEffect-fetch bugs — races, no cancellation, stale data, duplicate
requests), and shipping too much JavaScript.
Verified against the npm registry, 2026-08-01 — registry facts, not blog claims:
React 19.2.8 · Next.js 16.2.12 · Vite 8.2.0 · Vitest 4.1.10 ·
TypeScript 7.0.2 · TanStack Query 5.101 · Tailwind 4.3.3 ·
ESLint 10.8 (eslintrc is removed — see build-deploy.md) · Playwright 1.62 ·
MSW 2.15 · Zod 4.4.3.
When to Use
- Starting a web app, or adding a feature/route to one
- Choosing a framework, rendering strategy, state library, or styling approach
- Reviewing frontend code for correctness, performance or accessibility
- Diagnosing: slow LCP/INP, layout shift, large bundles, hydration mismatches, data races,
duplicate requests, stale UI after a mutation
- Setting up testing, linting, CSP, or bundle budgets
Not for: Node backend services (see node-microservice) or Flutter (see flutter-app).
Step 0: Choose the rendering strategy
This is the decision everything else follows from — and it's per-route, not per-app.
| Route characteristics |
Strategy |
| Public, SEO matters, content changes rarely |
SSG (+ ISR if it changes on a schedule) |
| Public, SEO matters, per-request data |
SSR / streaming SSR |
| Behind auth, no SEO value, app-like |
CSR (SPA) — genuinely the right answer for admin tools |
| Mixed: static shell, dynamic islands |
RSC (Next.js App Router) or Astro islands |
| If… |
Use |
| Public product surface, SEO + per-route flexibility |
Next.js 16 (App Router) |
| Internal tool behind auth |
Vite + React SPA — simpler mental model, no server to run |
| Content-heavy, minimal JS |
Astro |
| Want Remix data conventions |
React Router 7 |
Don't reach for SSR reflexively. An internal dashboard behind a login gets zero SEO
benefit from SSR and pays for it in complexity: a server to run, hydration mismatches, and
"is this code running on the server or the client?" on every line. Full reasoning in
references/frameworks.md.
Quick Reference
| Task |
Reach for |
Detail |
| Framework / rendering |
Next 16 · Vite SPA · Astro |
references/frameworks.md |
| Server state |
TanStack Query — never useEffect + fetch |
references/state-data.md |
| Client state |
useState/useReducer → Zustand/Jotai if it must be shared |
references/state-data.md |
| Forms |
React Hook Form + Zod resolver |
references/state-data.md |
| Styling |
Tailwind, or CSS Modules; zero-runtime CSS-in-JS |
references/styling.md |
| Components |
shadcn/ui (you own the code) or Radix primitives |
references/styling.md |
| Performance |
LCP / INP / CLS; code splitting |
references/performance.md |
| Accessibility |
Semantic HTML first; getByRole in tests |
references/accessibility.md |
| Tests |
Vitest 4 + RTL; Playwright for journeys; MSW |
references/testing.md |
| Build/deploy |
Vite config, ESLint 10 flat config, CSP, budgets |
references/build-deploy.md |
Bootstrap a new app
myapp/
src/
main.tsx / app/ # entry (or Next app router)
routes|pages/
features/<feature>/ # colocated: components + hooks + types
hooks/ # useThing() wrappers around useQuery
components/ui/ # design-system primitives
lib/ # api client, utils
tests/
eslint.config.js # flat config (ESLint 10 — eslintrc is REMOVED)
- TypeScript strict,
moduleResolution: "bundler", verbatimModuleSyntax.
- One HTTP client with a timeout by default —
fetch has none.
- TanStack Query for anything from the server, wrapped in per-concern custom hooks so
components call
useRecentActivity() and never touch a query key.
- ESLint 10 flat config + Prettier;
jsx-a11y rules on from day one — retrofitting
accessibility is far more expensive.
- Bundle-size budget in CI from the first commit. Budgets added later never pass.
- Error boundary + a real error tracker with source maps uploaded (not served publicly).
The non-negotiables
- Server state belongs in a data library, not
useState. useEffect + fetch + a
loading flag has no cancellation, races on fast navigation, and refetches nothing on
focus. This is the single most common React bug class.
- Every
fetch gets a timeout — AbortSignal.timeout(). There is no default.
- Semantic HTML first. A
<button> gives you focus, keyboard and role for free; a
<div onClick> gives you a bug report from a keyboard user.
- Never put a secret behind
VITE_/NEXT_PUBLIC_. Those are inlined into the bundle
and shipped to every visitor. This leaks keys regularly.
- Set width/height (or aspect-ratio) on images. Unsized images are the top cause of CLS.
getByRole over getByTestId. Tests that query the way assistive tech does are both
better tests and a live accessibility check.
- Code-split at route boundaries. One bundle for a ten-route app means paying for nine
routes nobody visited.
- Don't memo everything.
useMemo/useCallback have a cost; applied blindly they add
noise and hide the real problem. Measure first.
- Hashed assets immutable, HTML must-revalidate. Get this backwards and users are
stranded on a stale app or refetch everything constantly.
- Keep majors aligned across the org — especially validation libraries. Zod 3 in one
repo and Zod 4 in another means shared schemas are impossible.
Reference Map
| File |
Read when |
| references/frameworks.md |
Framework choice; CSR/SSR/SSG/ISR/RSC; the "use client" boundary |
| references/state-data.md |
Server vs client state; TanStack Query; the useEffect-fetch anti-pattern; forms |
| references/performance.md |
LCP/INP/CLS, bundles, images, fonts, rendering, React Compiler |
| references/styling.md |
Tailwind/CSS Modules/zero-runtime CSS-in-JS; tokens; component libs |
| references/accessibility.md |
Semantic HTML, focus management, ARIA, testing for a11y |
| references/testing.md |
Vitest + RTL query priority, MSW, Playwright, what not to test |
| references/build-deploy.md |
Vite/TS/ESLint config, env leaks, CSP, caching, budgets |
Common Mistakes
| Mistake |
Why it hurts |
Fix |
useEffect + fetch + loading state |
No cancellation; races on fast nav; stale data; duplicate requests |
TanStack Query |
fetch with no timeout |
Spinner forever; a hung request never resolves |
AbortSignal.timeout() |
Secret in VITE_/NEXT_PUBLIC_ |
Inlined into the bundle — shipped to every visitor |
Server-side only |
| Context as a state manager |
Every consumer re-renders on any change |
Zustand/Jotai, or split contexts |
<div onClick> |
No focus, no keyboard, no role |
<button> |
| Unsized images |
Layout shift — fails CLS |
width/height or aspect-ratio |
getByTestId everywhere |
Tests pass while the UI is unusable by keyboard/AT |
getByRole/getByLabelText |
| One bundle, no splitting |
Users download routes they never visit |
Route-level React.lazy |
Barrel files (index.ts re-exports) |
Defeats tree-shaking; pulls in the whole module graph |
Import direct paths |
useMemo on everything |
Cost without benefit; hides the real problem |
Profile, then memo |
Unstable key (array index) |
Wrong element reused; state attaches to the wrong row |
Stable IDs |
| Immutable HTML / revalidating assets |
Users stuck on a stale app, or no caching at all |
Hashed assets immutable; HTML revalidate |
| Public source maps |
Ships your source to anyone curious |
Upload to the error tracker only |
| Third-party scripts unaudited |
Usually the actual cause of bad INP |
Audit, defer, or drop them |
| React 18 patterns on 19 |
Missing use(), Actions, and Compiler benefits |
Read the 19 migration notes |
1---2name: web-frontend3description: Use when building, scaffolding, or reviewing a React/TypeScript web frontend — choosing between Next.js 16 App Router, Vite SPA, Remix/React Router 7 or Astro; CSR vs SSR vs SSG vs ISR vs React Server Components; TanStack Query for server state vs Zustand/Jotai for client state; Core Web Vitals (LCP/INP/CLS) and bundle size; Tailwind and design tokens; accessibility; Vitest + React Testing Library + Playwright; Vite config, ESLint flat config, CSP. Also for "start a web app", "my React app is slow", "fetch in useEffect", "which state manager", "Core Web Vitals failing", "hydration mismatch".4---56# Web Frontend78## Overview910A frontend is five things: a **rendering strategy**, **server state**, **client state**,11**a styling system**, and a **build/deploy pipeline**. Most React problems in production12trace to two root causes: **conflating server state with client state** (which produces the13whole family of `useEffect`-fetch bugs — races, no cancellation, stale data, duplicate14requests), and **shipping too much JavaScript**.1516Verified against the **npm registry, 2026-08-01** — registry facts, not blog claims:17React **19.2.8** · Next.js **16.2.12** · Vite **8.2.0** · Vitest **4.1.10** ·18TypeScript **7.0.2** · TanStack Query **5.101** · Tailwind **4.3.3** ·19**ESLint 10.8** (eslintrc is *removed* — see build-deploy.md) · Playwright **1.62** ·20MSW **2.15** · Zod **4.4.3**.2122## When to Use2324- Starting a web app, or adding a feature/route to one25- Choosing a framework, rendering strategy, state library, or styling approach26- Reviewing frontend code for correctness, performance or accessibility27- Diagnosing: slow LCP/INP, layout shift, large bundles, hydration mismatches, data races,28 duplicate requests, stale UI after a mutation29- Setting up testing, linting, CSP, or bundle budgets3031**Not for:** Node backend services (see `node-microservice`) or Flutter (see `flutter-app`).3233## Step 0: Choose the rendering strategy3435This is the decision everything else follows from — and it's per-route, not per-app.3637| Route characteristics | Strategy |38|---|---|39| Public, SEO matters, content changes rarely | **SSG** (+ ISR if it changes on a schedule) |40| Public, SEO matters, per-request data | **SSR / streaming SSR** |41| Behind auth, no SEO value, app-like | **CSR (SPA)** — genuinely the right answer for admin tools |42| Mixed: static shell, dynamic islands | **RSC** (Next.js App Router) or Astro islands |4344| If… | Use |45|---|---|46| Public product surface, SEO + per-route flexibility | **Next.js 16** (App Router) |47| Internal tool behind auth | **Vite + React SPA** — simpler mental model, no server to run |48| Content-heavy, minimal JS | **Astro** |49| Want Remix data conventions | **React Router 7** |5051**Don't reach for SSR reflexively.** An internal dashboard behind a login gets **zero** SEO52benefit from SSR and pays for it in complexity: a server to run, hydration mismatches, and53"is this code running on the server or the client?" on every line. Full reasoning in54references/frameworks.md.5556## Quick Reference5758| Task | Reach for | Detail |59|---|---|---|60| Framework / rendering | Next 16 · Vite SPA · Astro | references/frameworks.md |61| **Server state** | **TanStack Query** — never `useEffect` + `fetch` | references/state-data.md |62| Client state | `useState`/`useReducer` → Zustand/Jotai if it must be shared | references/state-data.md |63| Forms | React Hook Form + Zod resolver | references/state-data.md |64| Styling | Tailwind, or CSS Modules; zero-runtime CSS-in-JS | references/styling.md |65| Components | shadcn/ui (you own the code) or Radix primitives | references/styling.md |66| Performance | LCP / **INP** / CLS; code splitting | references/performance.md |67| Accessibility | Semantic HTML first; `getByRole` in tests | references/accessibility.md |68| Tests | Vitest 4 + RTL; Playwright for journeys; MSW | references/testing.md |69| Build/deploy | Vite config, ESLint 10 flat config, CSP, budgets | references/build-deploy.md |7071## Bootstrap a new app7273```74myapp/75 src/76 main.tsx / app/ # entry (or Next app router)77 routes|pages/78 features/<feature>/ # colocated: components + hooks + types79 hooks/ # useThing() wrappers around useQuery80 components/ui/ # design-system primitives81 lib/ # api client, utils82 tests/83 eslint.config.js # flat config (ESLint 10 — eslintrc is REMOVED)84```85861. TypeScript **strict**, `moduleResolution: "bundler"`, `verbatimModuleSyntax`.872. **One HTTP client** with a timeout by default — `fetch` has none.883. TanStack Query for anything from the server, wrapped in **per-concern custom hooks** so89 components call `useRecentActivity()` and never touch a query key.904. ESLint **10 flat config** + Prettier; `jsx-a11y` rules on from day one — retrofitting91 accessibility is far more expensive.925. **Bundle-size budget in CI** from the first commit. Budgets added later never pass.936. Error boundary + a real error tracker with source maps uploaded (not served publicly).9495## The non-negotiables96971. **Server state belongs in a data library, not `useState`.** `useEffect` + `fetch` + a98 `loading` flag has no cancellation, races on fast navigation, and refetches nothing on99 focus. This is the single most common React bug class.1002. **Every `fetch` gets a timeout** — `AbortSignal.timeout()`. There is no default.1013. **Semantic HTML first.** A `<button>` gives you focus, keyboard and role for free; a102 `<div onClick>` gives you a bug report from a keyboard user.1034. **Never put a secret behind `VITE_`/`NEXT_PUBLIC_`.** Those are inlined into the bundle104 and shipped to every visitor. This leaks keys regularly.1055. **Set width/height (or aspect-ratio) on images.** Unsized images are the top cause of CLS.1066. **`getByRole` over `getByTestId`.** Tests that query the way assistive tech does are both107 better tests and a live accessibility check.1087. **Code-split at route boundaries.** One bundle for a ten-route app means paying for nine109 routes nobody visited.1108. **Don't memo everything.** `useMemo`/`useCallback` have a cost; applied blindly they add111 noise and hide the real problem. Measure first.1129. **Hashed assets immutable, HTML must-revalidate.** Get this backwards and users are113 stranded on a stale app or refetch everything constantly.11410. **Keep majors aligned across the org** — especially validation libraries. Zod 3 in one115 repo and Zod 4 in another means shared schemas are impossible.116117## Reference Map118119| File | Read when |120|---|---|121| references/frameworks.md | Framework choice; CSR/SSR/SSG/ISR/RSC; the `"use client"` boundary |122| references/state-data.md | Server vs client state; TanStack Query; the useEffect-fetch anti-pattern; forms |123| references/performance.md | LCP/INP/CLS, bundles, images, fonts, rendering, React Compiler |124| references/styling.md | Tailwind/CSS Modules/zero-runtime CSS-in-JS; tokens; component libs |125| references/accessibility.md | Semantic HTML, focus management, ARIA, testing for a11y |126| references/testing.md | Vitest + RTL query priority, MSW, Playwright, what not to test |127| references/build-deploy.md | Vite/TS/ESLint config, env leaks, CSP, caching, budgets |128129## Common Mistakes130131| Mistake | Why it hurts | Fix |132|---|---|---|133| `useEffect` + `fetch` + `loading` state | No cancellation; races on fast nav; stale data; duplicate requests | TanStack Query |134| `fetch` with no timeout | Spinner forever; a hung request never resolves | `AbortSignal.timeout()` |135| Secret in `VITE_`/`NEXT_PUBLIC_` | Inlined into the bundle — shipped to every visitor | Server-side only |136| Context as a state manager | Every consumer re-renders on any change | Zustand/Jotai, or split contexts |137| `<div onClick>` | No focus, no keyboard, no role | `<button>` |138| Unsized images | Layout shift — fails CLS | width/height or aspect-ratio |139| `getByTestId` everywhere | Tests pass while the UI is unusable by keyboard/AT | `getByRole`/`getByLabelText` |140| One bundle, no splitting | Users download routes they never visit | Route-level `React.lazy` |141| Barrel files (`index.ts` re-exports) | Defeats tree-shaking; pulls in the whole module graph | Import direct paths |142| `useMemo` on everything | Cost without benefit; hides the real problem | Profile, then memo |143| Unstable `key` (array index) | Wrong element reused; state attaches to the wrong row | Stable IDs |144| Immutable HTML / revalidating assets | Users stuck on a stale app, or no caching at all | Hashed assets immutable; HTML revalidate |145| Public source maps | Ships your source to anyone curious | Upload to the error tracker only |146| Third-party scripts unaudited | Usually the actual cause of bad INP | Audit, defer, or drop them |147| React 18 patterns on 19 | Missing `use()`, Actions, and Compiler benefits | Read the 19 migration notes |