background-shape
React 18 Is Here, What Actually Changed for App Developers
April 1, 2022 · 6 min read · by Muhammad Amal programming

TL;DR — React 18 (released March 29, 2022) introduces concurrent rendering as opt-in via createRoot, automatic batching everywhere, useTransition + useDeferredValue for interruptible updates, Suspense for data fetching, Strict Effects in development. Upgrading is mostly a npm 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.
  • startTransition API. 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:

  1. Read the official upgrade guide — short
  2. Bump react and react-dom to ^18
  3. Replace ReactDOM.render with createRoot
  4. Run your test suite, watch for double-mount warnings from Strict Effects
  5. Fix any effect cleanup bugs that surface
  6. Ship to staging
  7. Don’t adopt new APIs yet — get to stable React 18 first, then layer in useTransition etc. 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:

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.