Upgrading a React 17 App to React 18, A Working Checklist
TL;DR — Upgrade
react+react-dom+ their@types/*. SwitchReactDOM.rendertocreateRoot. 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:
- Wait
- Use React 18 with the old library and accept some quirks (usually fine)
- 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
setXcalls (might needflushSync) - 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.