React 18 Is Here, What Actually Changed for App Developers
TL;DR — React 18 (released March 29, 2022) introduces concurrent rendering as opt-in via
createRoot, automatic batching everywhere,useTransition+useDeferredValuefor interruptible updates, Suspense for data fetching, Strict Effects in development. Upgrading is mostly anpm install+ a one-line API change.
April pivots to the frontend. After three months on backends — January’s containerization, February’s Postgres + CI, March’s Rust intro — I’ve been deliberately quiet about the React side of the stack. React 18 shipped four days ago (March 29, 2022). It’s the biggest React release since hooks. Time to look at it.
This first post is the headline summary: what’s in React 18, what changes for app developers (vs library developers — different concerns), and what’s worth diving into during the rest of April.
The headline: concurrent rendering, finally
The big one. Concurrent rendering has been in development since 2017’s “async React” announcement. It shipped in React 18 as opt-in: you get it the moment you upgrade to react@18 and switch from ReactDOM.render to ReactDOM.createRoot.
Before:
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
After:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
That’s the entire migration for the simplest case. Once you’re on the new root, you have concurrent features available. Existing code keeps working.
What concurrent rendering actually buys you:
- Interruptible renders. A long render can be paused if a higher-priority update arrives. The user-typed character beats the background list re-render.
startTransitionAPI. Mark some updates as “low priority” so they don’t block input.- Suspense improvements. Suspense now works for data fetching, not just code splitting.
- Better UX for slow components. The mental model shifts from “always synchronous” to “scheduling-aware.”
I’ll go deep on this in Monday’s post.
Automatic batching
React 17 batched state updates only inside event handlers. Updates outside (in promises, setTimeout, native event handlers) caused multiple re-renders. React 18 batches all of them.
Concrete: if you do
function handleClick() {
fetch('/api').then(() => {
setCount(c => c + 1); // React 17: re-render
setText('done'); // React 17: re-render again
});
}
…React 17 re-rendered twice. React 18 batches into one render. Free perf win across most apps. Almost no migration cost.
Opt out (rarely needed) with flushSync. Detailed walkthrough on Wed Apr 6.
Suspense for data fetching
In React 17, Suspense was for code splitting (React.lazy) and that was it. In React 18, Suspense supports arbitrary data fetching — components can suspend on async work and the framework shows the fallback until they resolve.
<Suspense fallback={<Loading />}>
<UserProfile id={userId} />
</Suspense>
The UserProfile component can now throw a promise (or use a Suspense-compatible data layer like SWR 1.x or React Query 4 beta) and the framework handles the wait state.
Important caveat: Suspense for data fetching works best with a framework (Next.js, Remix) or a data library that supports it. Hand-rolled useEffect data fetching doesn’t suspend. More on Friday Apr 8.
New hooks
Five new hooks in React 18. The ones app devs will actually use:
useId — generates a stable unique ID per component. For accessibility (label + input pairing) without colliding across SSR.
function NameField() {
const id = useId();
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} type="text" />
</>
);
}
useTransition — marks state updates as non-urgent.
const [isPending, startTransition] = useTransition();
function handleFilterChange(q) {
setQuery(q); // urgent — update input immediately
startTransition(() => {
setFilter(q); // non-urgent — heavy re-render can be interrupted
});
}
useDeferredValue — like useTransition but for derived values.
const deferredQuery = useDeferredValue(query);
// expensive list rendering uses deferredQuery — keeps input responsive
useSyncExternalStore — for libraries integrating external state (Redux, Zustand, etc.). Not for app code directly.
useInsertionEffect — for CSS-in-JS libraries. Not for app code.
The first three are app-developer hooks. Deep dive on Apr 11.
Strict Effects (development-only)
In StrictMode, React 18 development now mounts components twice (mount → unmount → re-mount) to surface effect-cleanup bugs. This catches a class of subtle bugs where your effect didn’t properly clean up subscriptions, intervals, etc.
useEffect(() => {
const sub = subscribe();
return () => sub.unsubscribe(); // critical — Strict Effects will mount/unmount/remount to verify
}, []);
If your effect doesn’t have proper cleanup, you’ll see double subscriptions, double fetches, etc. in dev. Production is unaffected.
This is going to surface bugs in apps that were “working” because the double-mount never happened. Fix the cleanups; ship the better code.
Server Components (still alpha)
React 18 mentions Server Components in the docs. They’re not production-ready in plain React 18. They land properly in Next.js 13 / Remix later in 2022. Skip for now; revisit when the framework support is stable.
What you should do this week
If you maintain a React app:
- Read the official upgrade guide — short
- Bump
reactandreact-domto ^18 - Replace
ReactDOM.renderwithcreateRoot - Run your test suite, watch for double-mount warnings from Strict Effects
- Fix any effect cleanup bugs that surface
- Ship to staging
- Don’t adopt new APIs yet — get to stable React 18 first, then layer in
useTransitionetc. one feature at a time
For most apps, the upgrade itself is a half-day of work. The opportunity to adopt new patterns is months of work. Take the upgrade win first.
What this month covers
Rest of April walks through the React 18 features and the Next.js patterns that pair with them:
- Apr 4: Concurrent rendering, useTransition, useDeferredValue
- Apr 6: Suspense for data fetching
- Apr 8: Automatic batching
- Apr 11: New hooks: useId, useSyncExternalStore
- Apr 13: Upgrading React 17 → 18
- Apr 15: Migrating CRA to Next.js 12
- Apr 18: Next.js routing patterns
- Apr 20: Next.js data fetching: SSG, SSR, ISR
- Apr 22: Next.js middleware for auth/redirects
- Apr 25: Image optimization with next/image
- Apr 27: Vercel vs self-hosted Next.js deploy
- Apr 29: April retro
What I’m explicitly NOT covering
- React Server Components beyond a mention. Not production-ready in 2022 H1.
- Remix. Worth covering separately — too big a topic to squeeze in.
- React Native. Different problem space.
- State management libraries (Redux, Zustand, Recoil, Jotai). The “right” choice depends on your team; the 18-specific changes are minor.
Common Pitfalls
Skipping the createRoot switch. Without it, you don’t get concurrent rendering. Library code can still use render for backwards-compat; app code should switch.
Adopting useTransition everywhere. It’s for specific slow renders. Most updates should stay urgent. Profile first; transition the ones that actually block input.
Mixing React 17 and React 18 in the same app. Don’t. The new root is intentionally incompatible with the old. Migrate the whole tree.
Believing your tests don’t need updates. React Testing Library 13+ targets React 18; older versions won’t work. Check your testing libs.
Treating Strict Effects warnings as React bugs. They’re surfacing your bugs. Fix the effect cleanup.
Adopting Server Components in production. Not yet. Wait for stable framework support.
Wrapping Up
React 18 is a foundational release. The migration is small; the opportunity is large. Most teams should ship the upgrade this month, then layer in concurrent features over the next quarter. Monday: concurrent rendering with useTransition and useDeferredValue — the API you’ll actually reach for first.