Reviewing React 18 Features: A TypeScript Guide
React 18 was the release that fundamentally changed how React handles rendering. Not incremental improvements -- a new mental model. Here are the features...
29 Apr 2024

React 18 was the release that fundamentally changed how React handles rendering. Not incremental improvements -- a new mental model. Here are the features that actually matter and how to use them in TypeScript.
Automatic batching
Before React 18, state updates inside setTimeout, promises, or native event handlers triggered a separate re-render for each update. React 18 batches them all together automatically.
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// React 18: one re-render
// React 17: two re-renders
}, 1000)
This is a free performance win. You don't have to change any code. React 18 batches updates everywhere, not just inside React event handlers.
useTransition
Some updates are urgent (typing in an input). Some are not (filtering a large list based on that input). useTransition lets you tell React which is which.
const [isPending, startTransition] = useTransition()
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value) // urgent: update the input immediately
startTransition(() => {
setFilteredResults(filterLargeList(e.target.value)) // non-urgent: can be interrupted
})
}
React keeps the UI responsive by yielding to urgent updates. If the user types another character before the transition finishes, React abandons the stale work and starts fresh. The result: the input never feels laggy, even when the background computation is heavy.
Suspense for data fetching
Suspense lets you declaratively handle loading states. Wrap a component that might suspend in a <Suspense> boundary:
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>
When UserProfile is waiting for data, React shows the <Spinner />. When data arrives, it swaps in the real content. No isLoading state variables. No conditional rendering. The loading logic is at the boundary level, not inside every component.
The trade-offs
To use React 18 features, you must use createRoot instead of ReactDOM.render. This is a one-time migration but it can break third-party libraries that haven't updated.
Concurrent rendering introduces a new class of bugs. Components can render, get interrupted, and re-render with different data. Side effects in render (which were always wrong but often harmless) now cause visible problems.
useTransition only helps when you have genuinely expensive computations. If your app is already fast, it adds complexity without benefit.
React 18 is worth upgrading to. But profile first, adopt features incrementally, and test your third-party dependencies before flipping the switch.