HTML / CSS

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

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 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.

Text
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.

Text
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:

Text
<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.