Skip to content
Back to Maintenance & LTS

Major Version Upgrade

Bump a major dependency or framework (e.g. React 17→18, Node 16→18) without melting production.

Why

Major versions carry breaking changes that silently break at runtime even when compile passes — removed APIs, changed default behavior, new required config. Skipping majors compounds debt: the longer you wait, the more breaking changes stack, and a 3-major jump is far riskier than three 1-major jumps. Ecosystem support drops for old majors (security patches, library compat), so staying current is also a security posture. A disciplined major upgrade is a planned, reversible, reviewed event — not a Friday-afternoon surprise.

When

When the current major is within 6-12 months of EOL, or when a feature you need is only in the new major. Schedule in a low-traffic week, never during a release freeze.

How

1. Read the CHANGELOG / migration guide end-to-end; list every breaking change that touches your code
2. Branch from main: git checkout -b upgrade/react-18
3. Bump the version in package.json, run install, capture every typecheck/lint error
4. Codemod first: use official codemods (e.g. npx @codemod/cli) for mechanical renames before manual edits
5. Fix remaining breakages one file at a time, commit each fix separately for bisect-ability
6. Run the full test suite + e2e; add tests for any behavior the changelog says changed
7. Deploy to staging for at least one full business cycle; monitor error rate and key metrics
8. Roll out with feature flags or canary if available; keep the old version warm for instant rollback

Example

# React 17 -> 18 upgrade
git checkout -b upgrade/react-18
npm install react@18 react-dom@18
npx tsc --noEmit   # surface type errors

# Codemod: replace ReactDOM.render with createRoot
npx @codemod/cli --plugin react-18-upgrade src/

# Manual: update index.tsx
- import { render } from "react-dom"
+ import { createRoot } from "react-dom/client"
- render(<App/>, document.getElementById("root"))
+ createRoot(document.getElementById("root")!).render(<App/>)

# Verify StrictMode double-invoke doesn't break effects
npm test && npm run e2e

Common Problems

  • ⚠Upgrading multiple majors at once (e.g. 16→18) — the breaking changes interact and you can't tell which upgrade caused which failure.
  • ⚠Trusting typecheck alone — runtime behavior changes (new StrictMode double-invoke, changed event pooling) pass types but break effects.
  • ⚠Forgetting transitive deps: bumping React 18 but leaving @testing-library/react at 12 causes silent test failures.
  • ⚠Deploying on a Friday and losing the rollback window — if Monday breaks, you can't tell the upgrade from the weekend's other changes.

Best Practices

  • ✓Upgrade one major at a time with a full test+staging cycle between each — three 1-major jumps are cheaper than one 3-major jump.
  • ✓Keep a running "upgrade backlog" board: every breaking change from the changelog becomes a ticket, so nothing is missed.
  • ✓Use feature flags to ship the upgrade behind a toggle, letting you revert in seconds without a redeploy if runtime issues appear.
  • ✓Schedule upgrades in Q1/Q2, never Q4 (holiday freeze) or the week before a major launch — give yourself a rollback runway.