Every React upgrade used to be additive — new hooks, new APIs, same mental model. React 19 is different. Server Components, Actions and the compiler together change the default answer to "where does this code run," and teams that keep their pre-19 instincts end up shipping more client JavaScript than they need to.
We've migrated six production apps onto React 19 this year. The wins were real, but only after we rebuilt the mental model — not the code — first.
The default flipped: server-first, not client-first
Before React 19, every component was implicitly a client component — you added a boundary to opt into server rendering. Now the default is the opposite: a component is a Server Component unless it explicitly says otherwise. That single flip changes how you should structure a new feature from the first line.
Where a component's code actually runs
- Push “use client” as deep as possible — the boundary should sit on the smallest interactive leaf (a button, a form), not on the page or layout that wraps it.
- Data fetching moves back into components— no more prop-drilling a fetch result through three layers just to avoid a waterfall; a Server Component can fetch exactly where it’s used.
- Client components can’t import server-only code — the build fails loudly instead of silently leaking a database credential into the browser bundle, which was previously a runtime-only mistake.
Actions replace half your API routes
Server Actions let a form submit directly to a server function — no separate API route, no manual fetch, no client-side state machine for loading/error/success. For CRUD-shaped mutations (the majority of what a typical SaaS ships), this removes an entire category of boilerplate.
Actions are for form-shaped mutations triggered from React. Anything a third party calls directly — webhooks, a mobile client, a public API — still needs a real route handler. Don't force those through an Action just for consistency.
The compiler removes most useMemo/useCallback
The React Compiler (stable as of 19) auto-memoizes components and values based on static analysis of your code, not manual dependency arrays. In our migrations, roughly 90% of hand-written useMemo/useCallback calls became redundant — the compiler already does that work, correctly, without a dependency array to get wrong.
- 1Remove memoization hooks after confirming the compiler is enabled — don't remove them speculatively first, or you'll reintroduce real re-render bugs on older React versions mid-migration.
- 2Keep useMemo only for genuinely expensive, non-rendering computation (a large sort, a heavy parse) — the compiler optimizes render behavior, not arbitrary CPU work.
- 3Run the compiler's ESLint plugin in CI before flipping it on in production — it flags the handful of patterns (mutating props, conditional hooks) that break its assumptions.
38%
average client bundle size reduction after moving to Server Components
90%
of hand-written useMemo/useCallback calls the compiler made redundant
6
production apps migrated this year with zero rollback
“We stopped asking “should this be a client component” and started asking “does this specific element need to react to a click.” Everything else answers itself.”
Key Takeaways
- React 19's real default is server-first — write plain components, and only add "use client" on the smallest interactive leaf.
- Server Actions replace most CRUD-shaped API routes; keep real route handlers only for endpoints called from outside React.
- The compiler handles most memoization automatically — audit and remove hand-written useMemo/useCallback after confirming it's enabled, not before.
- A build-time error when client code imports server-only modules catches secret leaks that used to be silent runtime bugs.
Where to start
Don't attempt a big-bang rewrite. Pick one new feature, build it server-first with the smallest possible client boundary, and use it to recalibrate the team's instincts before touching anything that already works.







