React Modernization ·Dec 2025 ·17 min read ·9.1K reads

React 16 → 19 at Scale:
Zero-Downtime Migration Playbook

A step-by-step guide to upgrading React across a 380-component codebase serving 40M+ users — codemods, Server Components adoption, and how we kept 100% uptime throughout every stage.

BG
Balachandraiah Gajwala
Senior Software Engineer (SDE3) · Intuit, Bengaluru
40M+
Users, zero downtime
~55%
Client JS reduced
3 mo
Full migration time

Why We Had to Upgrade

In late 2024 our QuickBooks web app was running React 16 — the same version we shipped in 2019. With React 19's new Compiler, Server Components, and Actions dramatically reducing client-side JavaScript and eliminating manual memoization, the performance gains were too significant to ignore for a platform serving 40M+ users.

React 19 isn't just a version bump. The new React Compiler eliminates virtually all manual memoization (useMemo, useCallback) and Server Components can reduce your client bundle by 40–60% on data-heavy pages.

The Migration Roadmap

Going directly from React 16 to 19 is a recipe for disaster. We staged it across 4 hops — each small, testable, and independently deployable.

Step 01
React 16 → 17 (Week 1)
Zero breaking changes for our codebase. Mostly a no-op. Just run the codemod and verify tests pass. This is your free confidence boost.
Step 02
React 17 → 18: New Root API (Week 2–3)
The first real code change — migrate from ReactDOM.render() to createRoot(). Also adopt Concurrent Features carefully. Took us 2 weeks due to 380+ component audit.
Step 03
Eliminate All Class Components (Weeks 4–7)
47 class components converted to function components. Tackled simplest first, most complex (those using getSnapshotBeforeUpdate) last. React Compiler requires function components.
Step 04
React 18 → 19: Compiler + Server Components (Weeks 8–12)
Adopted the React Compiler (removed 847 useMemo + 312 useCallback calls). Incrementally moved data-fetching components to Server Components using the leaf-first strategy.

The Root API Change (React 18)

// ❌ Before — React 16 (deprecated)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// ✅ After — React 18+ (createRoot)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

Running the Codemods

Before manually touching any file, run the official React codemods. They handle ~80% of mechanical changes automatically — prop-types removal, ref forwarding updates, defaultProps deprecations.

# React 19 official codemod suite — run this first!
npx codemod@latest react/19/migration-recipe

# Individual transforms we ran
npx codemod react/prop-types-typescript   # removes PropTypes
npx codemod react/replace-string-ref      # string refs → useRef
npx codemod react/replace-act-import      # test helper updates
npx codemod react/replace-default-export  # named exports
Always run codemods on a fresh git branch and review every single diff. They're good but not perfect — especially around complex ref patterns and legacy lifecycle methods.

Server Components: The Leaf-First Strategy

We used the leaf-first strategy — start with the outermost data-fetching components (dashboard summaries, report headers) and convert them first. Highest impact on bundle size, lowest risk to interactivity.

// ✅ Server Component — zero client JS, direct data access
async function DashboardSummary() {
  // Direct DB call — no useEffect, no loading spinner
  const data = await fetchQuarterlyMetrics();
  return <SummaryCard metrics={data} />;
}

// ✅ Client Component — only where interactivity is needed
'use client';
function FilterBar({ onFilter }: Props) {
  const [active, setActive] = useState('all');
  return (
    <div>
      {['all', 'paid', 'pending'].map(f => (
        <button key={f} onClick={() => { setActive(f); onFilter(f); }}>
          {f}
        </button>
      ))}
    </div>
  );
}

The React Compiler Win

After enabling the React Compiler in React 19, we did a full codebase audit and removed every manual memoization wrapper that the compiler now handles automatically.

❌ BEFORE
847 useMemo calls
✅ AFTER
0 useMemo calls
❌ BEFORE
312 useCallback calls
✅ AFTER
0 useCallback calls
❌ BEFORE
47 class components
✅ AFTER
0 class components

The Deployment Strategy: Feature Flags

We used feature flags to control the React 19 rollout across our 40M+ user base. Automated Playwright smoke tests ran after every percentage increase. Rollback was a single config change — no code deploy needed.

  1. 5% of new users → 2 weeks monitoring → P99 latency stable ✅
  2. 20% of users → 1 week monitoring → error rate stable ✅
  3. 50% of users → 1 week monitoring → Core Web Vitals improved ✅
  4. 100% rollout → full migration complete ✅
The feature flag rollout strategy was the difference between a scary big-bang release and a boring, uneventful upgrade. Boring deployments are the best deployments.
React 19 Next.js 15 Server Components React Compiler TypeScript Migration Intuit