Upgrading React 17 to 18: Root API, Batching, and Test Fixes
What changes when you upgrade React 17 to 18: the new root API, automatic batching, Strict Mode's double-invoked effects, and test setup fixes.
· 2 min read
React 18’s headline change for most apps isn’t a feature, it’s the new root API: ReactDOM.render is deprecated in favor of createRoot, and that one swap is what turns on automatic batching, concurrent rendering, and Strict Mode’s double-invoked effects. Here’s what breaks and what to fix when you make that swap.
Updating dependencies
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5"
},
"devDependencies": {
"@cfaester/enzyme-adapter-react-18": "^0.8.0"
}
}
npm install react@18.2.0 react-dom@18.2.0 react-redux@8.0.5
npm install --save-dev @cfaester/enzyme-adapter-react-18@0.8.0
npm uninstall @wojtekmaj/enzyme-adapter-react-17
Fixing the test setup
TextEncoder/TextDecoder go missing in some test environments after the upgrade. Add them to src/setupTests.js:
import { TextEncoder, TextDecoder } from 'util';
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
If you’re on Enzyme, swap the adapter in the same file:
import { configure } from 'enzyme'
import Adapter from '@cfaester/enzyme-adapter-react-18'
configure({ adapter: new Adapter() })
Switching to the root API
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
This replaces ReactDOM.render().
What the new root API changes
- Automatic batching: state updates are now batched even inside promises, timeouts, and native event handlers, not just inside React event handlers. Code that relied on a state update applying synchronously, right after the call that triggered it, needs to account for that.
- Strict Mode double-invokes effects: mount, unmount, mount, specifically to surface effects that don’t clean up after themselves. A
useEffectthat was quietly leaking a subscription or a timer becomes visible here. - New hooks become available:
useId,useTransition,useDeferredValue. Nothing forces you to adopt them immediately. act()’s behavior changed slightly. If a test suite leaned on its exact semantics, expect a few assertions to need adjusting.
The upgrade order that works
Switch the root API call first, since it’s what the rest of the migration hinges on, then run the test suite and fix whatever Strict Mode’s double-invoked effects expose. Most of what looks like a React 18 bug is actually an effect that was already leaking a subscription or a timer; the framework just started telling you.