Skip to content

React PWA

The default shape for user-facing apps: a client-rendered SPA, installable, offline-capable, deployable as static files.

Defaults

Concern Choice
Build Vite
Framework React + React Router (SPA/declarative mode)
Service worker vite-plugin-pwa (Workbox)
Styling Tailwind CSS
Components shadcn/ui — copied in and owned, not a dependency
UI state Zustand
Local data Dexie + useLiveQuery (see local-first.md)
Server state TanStack Query — only when there is a server cache to manage
Forms react-hook-form + Zod resolver

Rules for agents

  • Client-rendered and statically hosted by default. Do not introduce SSR without a stated reason (SEO, first-paint budget) — it costs the offline story.
  • Pick the state tool by shape: local-first reads come from Dexie live queries, not a fetch cache. Do not wrap IndexedDB in TanStack Query.
  • Zustand holds ephemeral UI state only. Anything durable belongs in IndexedDB or the backend.
  • shadcn/ui components are copied into the repo and edited freely — treat them as your code, not vendor code.
  • Ship the PWA essentials properly: web app manifest, maskable icons, offline fallback route, and a tested update prompt. An unhandled service-worker update is a stale app.
  • Never cache authenticated API responses in the service worker.
  • Route-level code splitting via lazy routes from the start.
  • Accessibility is not optional — accessibility.md. Degrade gracefully when the network fails — frontend-resilience.md. Store time in UTC and money as integers — localization.md.

Deviate when

Content sites that need SEO and fast first paint are a different problem — use a static site generator or SSR framework and say so explicitly.

Smells

useEffect fetching in a component, business logic in JSX, a service worker caching everything including /api, dangerouslySetInnerHTML, a manifest with no maskable icon, global CSS fighting Tailwind.