React 18 was released years ago, but in 2025 understanding it deeply is still crucial.
Why?
Because almost every modern React concept—Suspense, transitions, Server Components, streaming SSR, selective hydration—depends on the React 18 architecture.
This post explains React 18’s core features with depth, clarity, and real-world reasoning.
Let’s go.
🚀 1. Concurrent Rendering (The Engine Behind Everything)
Concurrent Rendering isn’t a mode anymore—React 18 made it the default engine when you use createRoot().
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
What concurrency gives you:
- React can pause rendering
- React can resume rendering
- React can cancel stale renders
- React can prepare UI in the background
- React can prioritize urgent work (typing/clicking)
This means no more UI jank during expensive updates.
Important detail
React still uses one thread.
Concurrency means interruptible rendering, not parallel rendering.
⚡ 2. Automatic Batching Everywhere
React batches setState calls automatically—no matter where they happen.
Before React 18 → batching only happened inside event handlers.
After React 18 → batching works in:
- timeouts
- promises
- async/await
- fetch handlers
- transitions
- any micro/macro task
Example:
setTimeout(() => {
setA(1);
setB(2);
});
// React 17 → 2 renders
// React 18 → 1 render ✔
If you ever need synchronous updates:
import { flushSync } from 'react-dom';
flushSync(() => setState(1));
Used sparingly.
🌀 3. Transitions for Non-Urgent Updates
Transitions separate urgent updates (typing, clicking) from non-urgent ones (filtering, heavy rendering).
const [isPending, startTransition] = useTransition();
startTransition(() => {
setFilteredList(heavyFilter(query));
});
What transitions solve
Without them, heavy UI work can block typing.
With them, typing stays fast, heavy work happens in the background.
Important detail
If the user types quickly → React cancels older transitions and only runs the latest one.
This is key to keeping UI responsive.
🌀 4. useDeferredValue for Delayed Values
A simpler alternative to transitions.
const deferredQuery = useDeferredValue(query);
const results = heavyFilter(deferredQuery);
-
queryupdates instantly -
deferredQueryupdates at lower priority
Important detail
This is not debounce.
It doesn’t wait for time—it waits for priority.
🧩 5. Suspense (Fully Async in React 18)
Before React 18, Suspense only worked with lazy().
Now Suspense works with any async operation.
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
Supports:
- data fetching
- streaming SSR
- selective hydration
- server components
- transitions
-
use()(React 19+)
Suspense pauses only the boundary, not the entire app.
🌊 6. Streaming SSR + Selective Hydration
React 18 introduced streaming SSR with:
renderToPipeableStream(); // Node
renderToReadableStream(); // Web Streams
This allows HTML to stream in chunks, not all at once.
Suspense + streaming = super fast TTFB
React streams fallback immediately →
Streams real content when it’s ready.
🟩 Understanding Selective Hydration
Selective hydration is fully automatic in React 18.
React evaluates:
- which components are visible
- which ones are interactive
- which ones have effects
- which ones live inside a Suspense boundary
Hydration priority:
1️⃣ Visible components hydrate first
2️⃣ Clicked or interacted components hydrate immediately
3️⃣ Above-the-fold content hydrates early
4️⃣ Below-the-fold content hydrates later
5️⃣ Suspense boundaries hydrate independently
6️⃣ Non-interactive or background UI hydrates when the browser is idle
You do not manually trigger selective hydration—React makes the decision dynamically.
This is one of the biggest performance wins in React 18.
🌱 7. New Root API (createRoot / hydrateRoot)
New APIs:
createRoot() // client entry
hydrateRoot() // hydration entry
These unlock:
- concurrency
- transitions
- async Suspense
- streaming
- selective hydration
Using the old ReactDOM.render() forces React into legacy mode
(no concurrency, no async Suspense, no selective hydration).
🆔 8. useId() — Stable, SSR-Safe IDs
Important for forms, ARIA, accessibility, and ensuring hydration correctness.
const id = useId();
<label htmlFor={id}>Email</label>
<input id={id} />
Why it's important
Random IDs or index-based IDs break hydration because:
- server and client generate different values
- the DOM mismatches
- React throws hydration warnings
- event binding breaks
useId() generates deterministic IDs based on component position,
so server and client produce the same result.
🔌 9. useSyncExternalStore (Required for State Libraries)
React 18 introduced this to ensure external state libraries work correctly with concurrency.
Used by:
- Redux Toolkit
- Zustand
- Jotai
- Recoil
const state = useSyncExternalStore(subscribe, getSnapshot);
Prevents “tearing” (seeing inconsistent UI across renders).
🎨 10. useInsertionEffect (For CSS-in-JS)
Runs before layout effects—used internally by emotion, styled-components, and other CSS-in-JS libraries.
You rarely use it directly, but it ensures styles don’t flicker.
useInsertionEffect(() => {
injectStyles();
});
🧪 11. Strict Mode Improvements
React 18’s Strict Mode intentionally double-invokes:
- render
- effects
- cleanup
This prepares developers for future React features (like automatic memoization and React Compiler).
This happens only in development.
🔍 12. DevTools Transition Tracing
Transition debugging tools let you see:
- which transitions ran
- what triggered them
- which renders got interrupted
- how long each took
Huge for performance tuning.
🧱 Final Thoughts
React 18 wasn’t “just another version.”
It was a new rendering model.
To fully understand modern React (2025+), you must understand:
- interruptible rendering
- priority scheduling
- async Suspense
- transitions
- deferred values
- streaming SSR
- selective hydration
- deterministic IDs
- concurrency-safe state stores
Master these foundations →
you understand React 19, Server Components, Next.js App Router, and everything modern React depends on.
Top comments (0)