Problem: Your Stack Is Running on Last Year's Defaults
You're on React 18, TypeScript 5.3, and Node 20. Everything works — but you're missing meaningful performance gains, new primitives, and security patches. Upgrading piecemeal leads to version mismatches that are harder to debug than a full planned migration.
You'll learn:
- How to audit your current stack and identify safe upgrade paths
- The exact sequence to upgrade frontend, backend, and infra without downtime
- What actually changed in 2026 models and what you can skip
Time: 60 min | Level: Advanced
Why This Happens
Stacks drift. You pin versions for stability, ship features, and suddenly you're three major versions behind. In 2026, this matters more than it used to — React 19's compiler, TypeScript 5.7's stricter inference, and Node 22 LTS aren't cosmetic upgrades. They change how your code runs.
Common symptoms:
- Bundle sizes 20-30% larger than peers using newer tooling
- Type errors suppressed with
anythat newer TS would catch automatically - Missing security patches (Node 20 EOL hit in April 2026)
- CI pipelines taking longer because of outdated build tooling
Solution
Step 1: Audit Before You Touch Anything
Run a full dependency snapshot first. You want a rollback point and a clear picture of what's outdated.
# Snapshot current state
node -v > .stack-audit.txt
npm -v >> .stack-audit.txt
npx tsc --version >> .stack-audit.txt
# Check outdated packages
npm outdated >> .stack-audit.txt
# Check for known vulnerabilities
npm audit >> .stack-audit.txt
Then check your actual usage of deprecated APIs — this is where most breakage hides:
# Install the migration linter for React 19
npx react-codemod@latest --list
# TypeScript: run strict mode as a dry-run without changing tsconfig
npx tsc --strict --noEmit 2> ts-strict-errors.txt
wc -l ts-strict-errors.txt
Expected: You'll get a count of strict-mode violations. Under 50 is a quick weekend fix. Over 200 means plan two weekends and fix TS first.
If it fails:
tscnot found: Runnpm install -D typescript@latestfirst- codemod crashes on JSX: Add
--parser=tsxflag
Step 2: Upgrade Node and Package Manager
Node 22 LTS is the 2026 baseline. Upgrade the runtime before touching framework versions.
# Using nvm (recommended)
nvm install 22
nvm use 22
nvm alias default 22
# Verify
node -v # Should show v22.x.x
# Update npm to v11
npm install -g npm@11
# If you use pnpm (faster for monorepos)
npm install -g pnpm@9
After switching Node versions, rebuild native dependencies:
# Clears and rebuilds node_modules for the new Node ABI
npm rebuild
# Or with pnpm
pnpm rebuild
Expected: node -v returns v22.x.x, no ABI mismatch errors on npm start.
If it fails:
- ABI mismatch on
better-sqlite3or similar native modules: Runnpm rebuild <package-name>individually - Scripts fail with
ERR_UNKNOWN_FILE_EXTENSION: Yourpackage.jsonis missing"type": "module"— either add it or rename files to.cjs
Step 3: Upgrade TypeScript
Go to 5.7 directly. Don't stop at 5.5 or 5.6 — 5.7 includes the narrowing improvements that make the strict-mode fixes from Step 1 much easier.
npm install -D typescript@5.7 @types/node@22
Update tsconfig.json for 2026 defaults:
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2024", "DOM", "DOM.Iterable"],
// 2026 strict defaults — enable these one at a time if migrating legacy code
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// TS 5.7 performance — enables incremental with isolated declarations
"isolatedDeclarations": true,
"incremental": true,
"skipLibCheck": false, // Stop skipping — fix your types
"outDir": "./dist",
"rootDir": "./src"
}
}
Run the compiler and fix errors in this order: 1) null/undefined issues, 2) implicit any, 3) exact optional properties. Don't fix them all at once — commit after each category.
npx tsc --noEmit
Expected: Zero errors before moving to the next step.
Step 4: Upgrade React and Next.js
React 19 and Next.js 15 go together. Don't upgrade one without the other.
npm install react@19 react-dom@19 next@15
npm install -D @types/react@19 @types/react-dom@19
Run the official codemods — they handle 80% of the mechanical changes:
# Removes deprecated lifecycle method patterns
npx react-codemod@latest rename-unsafe-lifecycles src/
# Updates to new JSX transform (removes React import from every file)
npx @next/codemod@latest next-async-request-api .
# Updates dynamic imports and metadata API
npx @next/codemod@latest metadata-to-viewport-export .
The two things codemods won't fix — you handle manually:
// 1. forwardRef is gone — use ref as a regular prop
// Before (React 18)
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));
// After (React 19)
function Input({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
// 2. Context no longer needs .Provider — use it directly
// Before
<ThemeContext.Provider value={theme}>
// After
<ThemeContext value={theme}>
Expected: next dev starts without warnings, pages render correctly.
If it fails:
useFormStatenot found: It's nowuseActionState— the codemod should have caught it, but check manually- Hydration errors after upgrade: React 19's stricter hydration catches mismatches that 18 silently ignored — fix the underlying mismatch in your server/client rendering
Step 5: Update Infrastructure
Docker base images and CI runners are easy to overlook. Mismatched runtime versions between local and production cause bugs that are painful to trace.
Update your Dockerfile:
# Before
FROM node:20-alpine
# After — use the slim variant for smaller images
FROM node:22-alpine3.19
WORKDIR /app
COPY package*.json ./
# Use cache mount for faster builds in 2026 Docker BuildKit
RUN --mount=type=cache,target=/root/.npm \
npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Update GitHub Actions:
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-24.04 # 22.04 goes EOL mid-2026
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
Verification
Run your full suite end-to-end:
# Type check
npx tsc --noEmit
# Unit tests
npm test
# Build
npm run build
# E2E (if you have Playwright)
npx playwright test
# Check bundle size didn't regress
npx bundlesize
You should see: All tests pass, build completes, bundle size is equal to or smaller than pre-upgrade (React 19 compiler typically reduces JS by 15-25%).
What You Learned
- Audit before upgrading — the strict-mode error count tells you how much time to budget
- Node runtime first, then TypeScript, then frameworks — order matters
- Codemods handle the mechanical work;
forwardRefremoval and Context API changes need manual review - Infrastructure (Docker, CI) must match your local Node version or you'll ship bugs that only appear in production
Limitations: This guide covers the React/Next.js + TypeScript path. Go, Rust, and Python backend upgrades follow the same audit-first principle but have different breaking change profiles — worth a separate pass.
When NOT to use this approach: If you're mid-feature freeze or within two weeks of a major release, wait. A planned stack upgrade done in a quiet sprint is always better than an emergency rollback.
Tested on macOS Sequoia 15.3, Ubuntu 24.04 LTS — Node 22.14, TypeScript 5.7.3, React 19.0, Next.js 15.2