Back to React
React
easy
mid

What are the key differences between React 16 and React 18?

React 18 added concurrent rendering (interruptible renders), automatic batching everywhere (not just in event handlers), Suspense for data fetching, useTransition / useDeferredValue, useId, the new createRoot API, server components (experimental), and stricter StrictMode that double-mounts effects in dev. React 16 introduced Fiber, hooks (16.8), and error boundaries but rendering was fully synchronous.

7 min read·~5 min to think through

Quick map of the most impactful changes.

Rendering model

  • React 16: fully synchronous render. Once started, a render couldn't be interrupted.
  • React 18: concurrent rendering. The reconciler can pause, abandon, or replay renders based on priority.

Batching

  • React 16/17: batched only inside React event handlers. setTimeout or fetch.then triggered a render per setState.
  • React 18: automatic batching everywhere — promises, native events, setTimeout, etc.

Root API

  • 16: ReactDOM.render(<App/>, root)
  • 18: createRoot(root).render(<App/>) — the new API is what unlocks concurrent features.

New hooks

  • useTransition / useDeferredValue — prioritization.
  • useId — stable IDs that work across server/client.
  • useSyncExternalStore — safe subscriptions to external stores (Redux, Zustand) under concurrent rendering.
  • useInsertionEffect — for CSS-in-JS libraries.

Suspense

  • 16: only worked with React.lazy for code-splitting.
  • 18: supports server-side streaming and data-fetching libraries (Relay, Next.js).

StrictMode

  • 18: in development, components mount → unmount → mount to surface effect cleanup bugs.

Server components (experimental in 18, GA via frameworks)

Render on the server, ship zero JS for that component, stream HTML. Adopted by Next.js App Router.

Hooks

Both have hooks (16.8 introduced them). React 17 was the famously 'no-new-features' release that prepped for concurrent.

Migration notes

  • Switch ReactDOM.rendercreateRoot (one-line change).
  • Audit useEffect for cleanup correctness — StrictMode double-mount will surface leaks.
  • TypeScript users: children prop is no longer implicit on FC.
  • Third-party stores: switch to useSyncExternalStore to be safe under concurrent.

When 'difference' is being asked

The interviewer usually wants three things: concurrent rendering, automatic batching, and the new hooks (Transition/DeferredValue/Id). Bonus credit for mentioning the StrictMode double-mount and the createRoot API.

Follow-up questions

  • What's the practical impact of automatic batching on existing code?
  • Why does React 18 StrictMode double-invoke effects?
  • How does useSyncExternalStore prevent tearing in concurrent mode?

Common mistakes

  • Claiming hooks were introduced in 18 — they shipped in 16.8.
  • Confusing concurrent mode (the React 17 experiment) with concurrent rendering (the 18 reality).
  • Forgetting that StrictMode behavior is dev-only.

Performance considerations

  • Automatic batching reduces wasted renders. Concurrent rendering doesn't make renders faster — it makes them yieldable, so the main thread stays responsive. The win is perceived performance, not raw throughput.

Edge cases

  • Some apps relied on un-batched updates inside promises; they'll see fewer renders after 18 (usually a win, occasionally a bug).
  • Legacy stores without useSyncExternalStore can tear under concurrent rendering.
  • Effect cleanup that wasn't idempotent will fail under the StrictMode double-mount.

Real-world examples

  • Next.js 13+ requires React 18 for App Router and Server Components. Most public-facing React apps in 2025 are on 18; libraries are mid-migration to useSyncExternalStore-safe subscriptions.

Senior engineer discussion

Senior framing: 18 is the foundation for Server Components and streaming. The hooks (Transition/DeferredValue) are nice but the deeper bet is that the reconciler is now interruptible, which unlocks an entire class of features the React team is still rolling out.

Related questions