Skip to content

Production & Ecosystem

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:

Terminal window
npm run build # produces the optimized production bundle

Never 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.

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.

React is deliberately minimal, so real apps assemble a few well-established libraries. The converged, current choices:

NeedReach for
Routing (SPA)React Router — or a framework’s built-in router (Next.js)
Server state / data fetchingTanStack Query (React Query), or RTK Query
FormsReact Hook Form (+ a schema validator like Zod)
Client stateZustand or Redux Toolkit (or Jotai) — beyond what Context comfortably handles
StylingCSS Modules, Tailwind, or a CSS-in-JS library
Error monitoringSentry (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"]
React plus the pieces a real app assembles

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.

  • 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).
What is the difference between React’s development and production builds?
How do you code-split a component so it loads on demand?
Why keep server state and client state separate?
What is the cost of leaving `<StrictMode>` on in production?