Production & Ecosystem
Dev build vs production build
Section titled “Dev build vs production build”React ships two builds. The development build is bigger and slower on purpose — it includes helpful warnings, prop-type checks, and the Strict Mode double-invoke. The production build strips all of that for speed and size. Your build tool (Vite / the framework) switches automatically when you run the production build:
npm run build # produces the optimized production bundleNever ship the development build — it’s meaningfully slower and larger. And remember <StrictMode> only double-invokes in development; in production it does nothing, so it costs you nothing to leave it on.
Code-splitting with lazy
Section titled “Code-splitting with lazy”A single big bundle means users download code for pages they may never visit. React.lazy + <Suspense> split a component into its own chunk that loads on demand:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard')); // separate chunk
function App() { return ( <Suspense fallback={<Spinner />}> <Dashboard /> {/* downloaded only when this renders */} </Suspense> );}Split at route boundaries first (each page becomes a chunk) — that’s the highest-value split, and frameworks often do it for you.
The ecosystem: what to reach for
Section titled “The ecosystem: what to reach for”React is deliberately minimal, so real apps assemble a few well-established libraries. The converged, current choices:
| Need | Reach for |
|---|---|
| Routing (SPA) | React Router — or a framework’s built-in router (Next.js) |
| Server state / data fetching | TanStack Query (React Query), or RTK Query |
| Forms | React Hook Form (+ a schema validator like Zod) |
| Client state | Zustand or Redux Toolkit (or Jotai) — beyond what Context comfortably handles |
| Styling | CSS Modules, Tailwind, or a CSS-in-JS library |
| Error monitoring | Sentry (with an error boundary at the app root) |
flowchart TB react["React (view layer)"] react --> route["Routing: React Router / framework"] react --> data["Server state: TanStack Query"] react --> forms["Forms: React Hook Form"] react --> state["Client state: Zustand / Redux Toolkit"]
A key distinction that saves a lot of pain: server state (data from an API — TanStack Query) and client state (local UI state — useState/Context/Zustand) are different problems. Trying to hold server data in a client store (manual loading flags, cache invalidation, refetching) is the classic reinvention that a data library solves for you.
A production checklist
Section titled “A production checklist”- Build with the production bundle; verify it’s not the dev build.
- Code-split at routes with
lazy+Suspense. - Put an error boundary at the app root (and around risky subtrees) and wire it to monitoring.
- Keep
<StrictMode>on — it’s a dev-only guard with zero production cost. - Separate server state (a data library) from client state (local/store).