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.
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.
setTimeoutorfetch.thentriggered 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.lazyfor 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.render→createRoot(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
useSyncExternalStoreto 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.