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.
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.
useMemo, useCallback) and Server Components can reduce your client bundle by 40–60% on data-heavy pages.Going directly from React 16 to 19 is a recipe for disaster. We staged it across 4 hops — each small, testable, and independently deployable.
ReactDOM.render() to createRoot(). Also adopt Concurrent Features carefully. Took us 2 weeks due to 380+ component audit.getSnapshotBeforeUpdate) last. React Compiler requires function components.// ❌ 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 />);
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
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>
);
}
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.
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.