Back

What React 19 Changes About Async Rendering

What React 19 Changes About Async Rendering

React 18 introduced concurrent rendering. React 19 doesn’t replace that foundation—it builds on it by giving you standardized patterns for async workflows you were previously hand-rolling yourself.

If you’ve been managing isPending states, coordinating error handling across async operations, or implementing optimistic updates manually, React 19 provides first-class APIs for all of it. This article explains what actually changed and how your mental model should shift.

For reference, all APIs discussed here are documented in the official React documentation: https://react.dev

Key Takeaways

  • React 19 builds on React 18’s concurrent rendering by adding higher-level abstractions for common async patterns
  • startTransition now accepts async functions, and these are formally referred to as Actions in React documentation
  • useActionState eliminates boilerplate for form handling with built-in pending state and error management
  • useOptimistic standardizes optimistic updates with rollback driven by canonical state
  • The use() API lets you read promises during render, but requires cached or Suspense-compatible promises

The Core Shift: From Manual Plumbing to Built-In Patterns

React 18 gave us the concurrent rendering engine. React 19 gives us the abstractions that make it practical.

Previously, handling an async form submission meant juggling multiple state variables:

const [isPending, setIsPending] = useState(false)
const [error, setError] = useState(null)

const handleSubmit = async () => {
  setIsPending(true)
  try {
    await submitData()
  } catch (e) {
    setError(e)
  } finally {
    setIsPending(false)
  }
}

React 19 Actions eliminate this boilerplate entirely.

React 19 Actions and Async Transitions

The biggest change is that startTransition now accepts async functions. In React 19, these async transition functions are formally referred to as Actions in the React documentation.

const [isPending, startTransition] = useTransition()

const handleSubmit = () => {
  startTransition(async () => {
    const error = await updateName(name)
    if (error) {
      setError(error)
      return
    }
    // perform navigation or state update on success
  })
}

React 19 tracks pending transition state for you, which you then read explicitly via APIs like useTransition or useActionState. All state updates inside startTransition batch together, producing a single re-render when the async work completes. This avoids intermediate states where isPending is false but error state hasn’t been applied yet.

useActionState: Purpose-Built Form Handling

For forms specifically, useActionState provides built-in pending state and result management:

const [error, submitAction, isPending] = useActionState(
  async (previousState, formData) => {
    const error = await updateName(formData.get("name"))
    if (error) return error
    // perform navigation or update application state on success
    return null
  },
  null
)

return (
  <form action={submitAction}>
    <input type="text" name="name" />
    <button disabled={isPending}>Update</button>
    {error && <p>{error}</p>}
  </form>
)

React now enhances the native form action attribute with integrated pending and result state handling.

useOptimistic: Instant UI Feedback

React 19 standardizes optimistic updates through useOptimistic:

const [optimisticName, setOptimisticName] = useOptimistic(currentName)

const submitAction = async (formData) => {
  const newName = formData.get("name")
  setOptimisticName(newName) // UI updates immediately
  const result = await updateName(newName)
  onUpdateName(result)
}

React reverts optimisticName to currentName when the parent component re-renders with the canonical state after the action resolves. For failed actions, ensure your parent state remains unchanged so the optimistic value reverts correctly.

React Suspense Changes and the use() API

The use() API lets you read promises directly during render. Unlike hooks, it works inside conditionals and loops:

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise)
  return comments.map(comment => <p key={comment.id}>{comment}</p>)
}

When the promise is pending, use() suspends the component. Wrap it in a Suspense boundary to show fallback UI.

Important constraint: use() doesn’t support promises created during render. The promise must be cached or come from a Suspense-compatible source. You also cannot use use() inside try-catch blocks—error boundaries handle rejections instead.

In server-capable environments, React documentation recommends preferring async/await for data fetching semantics, using use() primarily for consuming already-managed promises.

What Didn’t Change

React concurrent UI patterns—the underlying scheduling, interruptible rendering, and priority system—remain the React 18 foundation. React 19 doesn’t introduce new concurrency concepts. It provides higher-level APIs that leverage existing concurrent capabilities.

The mental model shift isn’t about understanding new rendering mechanics. It’s about recognizing that React now handles async UI states you previously managed yourself.

Practical Implications

Stop hand-rolling pending states. Use useTransition or useActionState instead of manual isPending variables.

Embrace Actions for mutations. Any async function wrapped in startTransition becomes an Action with transition-aware scheduling.

Use useOptimistic for responsive UIs. The pattern is now standardized, not something you implement ad-hoc.

Pair use() with Suspense for data fetching. But remember the caching requirement—this works best with frameworks or libraries that manage promise stability.

Conclusion

React 19 async rendering isn’t a new paradigm. It’s the missing abstraction layer that makes concurrent React practical for everyday async workflows. By providing built-in APIs for pending states, form handling, optimistic updates, and promise resolution, React 19 eliminates the boilerplate that developers have been writing since React 18 introduced concurrent rendering. The foundation remains the same—what’s changed is how accessible and standardized these patterns have become.

FAQs

Yes. Actions work in any React 19 application. While frameworks like Next.js provide additional server-side integration, the core APIs like useTransition with async functions, useActionState, and useOptimistic function in client-side React apps without any framework dependencies.

Not entirely. useOptimistic reverts to the original value when the parent component re-renders with unchanged canonical state. You must ensure failed actions do not update that state so rollback occurs correctly.

Not necessarily. useTransition is ideal for non-urgent updates where you want React to keep the current UI responsive. For critical loading indicators that should block interaction, traditional useState patterns may still be appropriate.

Technically yes, but it requires careful promise management. The promise passed to use() must be stable across renders, meaning you cannot create it during render. Use a caching layer, a data-fetching library, or framework-level data loading to ensure promise stability.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay