background-shape
Upgrading a React 17 App to React 18, A Working Checklist
April 13, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — Upgrade react + react-dom + their @types/*. Switch ReactDOM.render to createRoot. Run the test suite. Audit Strict Effects warnings in dev. Check that your state library, router, and form library are React-18-aware. Half a day for most apps.

After the overview and three feature posts, time to do the actual upgrade. This is the checklist I ran through on our internal admin app last week. Took half a day. Most apps will be similar.

Step 1: Upgrade the packages

npm install react@18 react-dom@18
npm install -D @types/react@18 @types/react-dom@18

If you use Yarn:

yarn add react@18 react-dom@18
yarn add -D @types/react@18 @types/react-dom@18

Done. The next steps are about adapting the code.

Step 2: Switch to createRoot

In your app entry (usually index.tsx or src/main.tsx):

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

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
if (!container) throw new Error('root element missing');
const root = createRoot(container);
root.render(<App />);

If you have SSR with hydrate, switch to hydrateRoot:

import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

This is the one-line change that opts your app into React 18’s new features. Without it, you’re running React 18 in compat mode — no concurrent rendering, no auto-batching for non-event-handler updates.

Step 3: TypeScript adjustments

If you’re on TS, React 18’s types are stricter in one notable place: children is no longer implicit on React.FC. You have to declare it:

// Before (React 17 types)
const Wrapper: React.FC = ({ children }) => <div>{children}</div>;

// After (React 18 types)
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => <div>{children}</div>;

Or skip React.FC entirely (the React docs now recommend this):

function Wrapper({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

Cleaner anyway. Most apps will see a flurry of TS errors on the first build; mechanical to fix.

Step 4: Test the build

npm run typecheck
npm run lint
npm run test
npm run build

Things to watch for:

  • Test failures from React Testing Library (need v13+ for React 18; older versions throw at startup)
  • TS errors as covered above
  • Library-specific deprecation warnings in console

Step 5: Audit Strict Effects warnings

In <StrictMode>, React 18 development now mounts every component twice (mount → unmount → re-mount) to surface effects with missing cleanup.

import { StrictMode } from 'react';

const root = createRoot(container);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

Open the app in dev. Watch the console. Network tab. Look for:

  • Duplicate API calls per page load
  • Subscriptions that fire twice
  • Event listeners that register twice
  • Animations or timers that double up

Each one is a real bug — your effect lacks proper cleanup. Fix:

// Before — broken in Strict Effects
useEffect(() => {
  const sub = subscribe();
  // forgot cleanup
}, []);

// After — works in Strict Effects
useEffect(() => {
  const sub = subscribe();
  return () => sub.unsubscribe();
}, []);

For one-shot fetches, you usually want to track an ignore flag or use AbortController:

useEffect(() => {
  const controller = new AbortController();
  fetch('/api', { signal: controller.signal })
    .then(setResponse)
    .catch(e => { if (e.name !== 'AbortError') throw e; });
  return () => controller.abort();
}, []);

Yes, this is more work. But the effect was already broken; Strict Effects just made it visible.

Step 6: Check library compatibility

The libraries you most likely depend on, and their React-18-ready versions:

Library Min React 18-ready version
React Router 6.4+ (in beta in April, stable later in 2022)
Redux 8.0+ (April 2022)
React Redux 8.0+
Zustand 4.0+
React Query (TanStack) 4.0+ beta
SWR 1.3+
React Hook Form 7.30+
Material UI 5.6+
Chakra UI 2.0+
Framer Motion 6.3+
Styled Components 5.3+ (works), 6.0 (full React 18 support)
Emotion 11.10+
React Testing Library 13.0+

Bump them. Run tests. Most are drop-in.

If a library you depend on hasn’t shipped a React 18 release yet, you have three choices:

  1. Wait
  2. Use React 18 with the old library and accept some quirks (usually fine)
  3. Pin React 17 for now

For most apps, (2) works. The library’s existing API doesn’t break; you just miss out on tearing-free updates from external stores. Not the end of the world.

Step 7: Validate in staging

Deploy to staging. Spend an hour clicking through critical user paths. Watch for:

  • Visual glitches (rare but possible from batching changes)
  • Forms that need values immediately after setX calls (might need flushSync)
  • Page transitions that previously relied on synchronous re-rendering

Most apps see nothing. Some apps see one or two surprises. None I’ve seen are showstoppers.

What you should NOT do as part of the upgrade

Don’t, in the same PR, do any of:

  • Convert components to use useTransition / useDeferredValue
  • Migrate to Suspense for data fetching
  • Adopt React Server Components

Those are features of React 18 to adopt over the next quarter. The upgrade itself is just “we’re on the new major.” Keep the PR small. Ship it. Then iterate.

Real timing

For the apps I’ve upgraded so far:

App Codebase size Upgrade time
Internal admin (50 components) small 2 hours
Customer dashboard (~300 components) medium half day
Marketing site (Next.js 12) small 30 min (Next.js handles it)

The bulk of the time is Strict Effects fix-up, not the upgrade itself. Apps with cleaner existing effect cleanup ship faster.

Common Pitfalls

Forgetting react-dom/client. createRoot is in react-dom/client, not react-dom. Imports from the wrong path fail with a confusing error.

Skipping the @types/* upgrade. TS will compile against React 17 types and produce wrong errors. Always upgrade types alongside react.

Not updating React Testing Library. Suite crashes with “React 18 not supported.”

Treating Strict Effects warnings as React bugs. They’re your bugs. Fix the cleanups.

Adopting Suspense + transitions in the upgrade PR. Too much. Separate PRs.

Skipping <StrictMode>. Without it, you don’t get Strict Effects in dev. You lose the bug-surfacing benefit. Always render under StrictMode in dev.

Forgetting that index.tsx is sometimes auto-generated. CRA, Vite, Next.js all have different entry conventions. Check your framework’s React 18 docs.

Wrapping Up

The React 18 upgrade is a half-day exercise. The win is automatic batching, the platform for future concurrent features, and the Strict Effects audit that catches real bugs. Friday’s post pivots to the framework side: migrating from CRA to Next.js 12 — the other half of frontend modernization.