The Complete Developer Guide to React 19, Part 1: Async Handling

Authors
Hristo Totov
Software Engineer
@
Callstack

React 19 beta has been available on npm for some time now. While this release is currently intended for library developers and maintainers to gather feedback and improve the future stable version, most of its features can be considered stable.

These newly added features aim to assist application developers in their day-to-day tasks, which include state, error, and asynchronous handling. If used correctly, these updates should improve workflows, make applications faster, and simplify development.

This guide aims to review the most notable features, explain when they could be helpful, and explain how to use them with practical examples and short explanations. It comes in three parts—this is just the first part, focusing on Async Handling with Hooks. Part two delves into React 19 improvements and additions, while the last part explores upgraded resource support and presents overall conclusions:

Enhanced Async Handling with Hooks

Actions

Actions are intended to handle asynchronous requests and automate managing pending states, errors, and optimistic updates. Developers can handle async operations more intuitively now while keeping the UI responsive at all times - even when requests are happening live. 

Monitoring state with variables like isPending or isLoading during async operations can now be replaced with the new useTransition hook. useTransition allows you to update the component state without blocking the user interface. In other words, it makes the application run and feel smoother while performing async operations in the background, like a network request. One example of how to use the useTransition hook would be:

function UpdateName({}) {
const [name, setName] = useState("");
const [error, setError] = useState(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect("/path");
})
};
return (
<div>
<input value={name} onChange={event => setName(event.target.value)}/>
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
{error && <p>{error}</p>}
</div>
);
}

What's important to notice here:

  1. startTransition is content meaning that whatever is wrapped inside will not trigger a state update until all of the code inside is executed. In other words, no matter how many state updates are performed inside startTransition (in the example above only one - setError) all of them will come through as one update at the end. This makes the hook extremely useful as it would only cause one UI rerender instead of N rerenders where N is the number of state changes performed inside handleSubmit.
  2. Previously we would have to use an isPending state variable to detect whether updateName has been completed and the code for that would look something like this:
const handleSubmit = async () => {
setIsPending(true);
const error = await updateName(name);
setIsPending(false);
if (error) {
setError(error);
return;
}
redirect("/path");
};
Notice that there might be a point where pending is set to false, but we don’t have the error just yet.

Now imagine if you had multiple async operations between setPending(false) and if(error) - as you can imagine it would not be a very nice user experience. This cannot happen in startTransition because it would produce only one state update at the end - after everything is completed. Moreover, it saves us one line of code!

  1. isPending will automatically be set to false after any transition which allows the developer to efficiently update the UI while the action is going and not have to worry about handling loading logic while the data is changing.
  2. Functions that use async transitions are called “Actions” by convention. Actions provide a pending state which automatically resets, supports optimistic updates, provides error handling to allow for Error Boundaries when a request fails and can also be passed to formform action props.

Better handling form

React 19 also introduces a new hook called useActionState which provides developers with built-in support for handling form states and submission. This hook allows for the automatic management of pending states and results. 

function UpdateName({ name, setName }) {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const error = await updateName(formData.get("name"));
if (error) {
return error;
}
redirect("/path");
return null;
},
null,
);
return (
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
{error && <p>{error}</p>}
</form>
);
}

Here are a few important things to notice:

  1. Everything inside useActionState is action code i.e. the code to be executed when the submit happens
  2. formData is not really type-safe, meaning it’s not guaranteed that formData.get(name) will return text. A custom hook that uses useActionState with a validator could come in handy in order to validate data before passing it to API calls.
  3. submitAction is directly passed in the <form /> element action prop. useActionState is specifically for actions and especially for forms
  4. ‍useActionState is specifically for actions and especially for forms.

In addition to useActionState, a new hook called useFormStatus has also been introduced to further simplify the usage of form elements in React 19. It allows developers to access form information in child components without having to drill down props or use context. The usage is straightforward and intuitive.

import {useFormStatus} from 'react-dom';
function DesignButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}

useFormStatus provides access to four form properties: pending, data, method, and action

const { pending, data, method, action } = useFormStatus();

An important thing to note here is that only child components of the form element would be able to access these properties i.e. DesignButton is a child component of <form /> in this example.

Optimistic updates

Optimistic updates are a way of instantly showing the user the final state of the UI optimistically even though data mutations are ongoing in the background. The application feels more responsive this way and it’s especially helpful in cases of bad connection or services that take a long time to resolve. In case of an error, the UI is reverted to the original state.

In React 19 this functionality can be implemented using the new useOptimistic hook.

function UpdateName({currentName, onUpdateName}) {
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
const submitAction = async formData => {
const newName = formData.get("name");
setOptimisticName(newName);
const updatedName = await updateName(newName);
onUpdateName(updatedName);
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input
type="text"
name="name"
disabled={currentName !== optimisticName}
/>
</p>
</form>
);
}

What's important here: 

  1. useOptimistic takes an initial value just like useState.
  2. React will automatically revert optimisticName back to currentName when the submit finishes or errors.

New API use

React 19 introduces a way to read resources like promises and context in render using use. What is more, this new API can be used in conditionals as well which was previously uncharted territory for hooks. This change allows developers to manage data dependencies in a more effective way and focus on creating more dynamic and well-designed applications.

Reading a promise with use can be coupled with Suspense until the promise resolves, which is handy for network calls, for example:

import {use} from 'react';
function Comments({commentsPromise}) {
// `use` will suspend until the promise resolves.
const comments = use(commentsPromise);
return comments.map(comment => <p key={comment.id}>{comment}</p>);
}
function Page({commentsPromise}) {
// When `use` suspends in Comments,
// this Suspense boundary will be shown.
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
view raw NewApi-use.jsx hosted with ❤ by GitHub

An important thing to note here is that use does not support promises created in render. If you want to pass a promise, it must be cached or use a Suspense-compatible library or framework. use can also be used for conditional rendering and to read from Context, allowing for useful theme switching. Note that it cannot be called in a try-catch block, though.

import {use} from 'react';
import LightThemeContext from './LightThemeContext';
import DarkThemeContext from './DarkThemeContext';
function Heading({children}) {
let theme;
if(theme === 'dark') {
theme = use(DarkThemeContext)
} else {
theme = use(LightThemeContext)
}
return (
<h1 style={{color: theme.color}}>
{children}
</h1>
);
}

An important thing to notice here is that there’s no need for context consumers and developers can just use use directly and obtain the context value even in conditional statements and early returns. Moreover, similar to useContext, use searches for the closest context provider that is above the component that calls it. This means that use does not consider context providers in the component from which it was called.

Server Components

React 19 introduces a way of rendering components ahead of time, either once at build time on a CI server or per each request using a web server. This happens in an environment separate from your client application called the “server”. If you have used Next.js before you might be familiar with the setup. 

With Server Components, React effectively allows developers to decide when the HTML file gets created—on a server, at build time, or at runtime, giving more flexibility and room for optimization.

Previously, a common way to read static data from a CMS, for example, would be to use an effect: 

// 75K gzipped libraries
import sanitizeHtml from 'sanitize-html';
import marked from 'marked';
function Page({page}) {
const [content, setContent] = useState('');
// NOTE: loads *after* first page render.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);
return <div>{sanitizeHtml(marked(content))}</div>;
}
// API
app.get(`/api/content/:page`, async (req, res) => {
const page = req.params.page;
const content = await file.readFile(`${page}.md`);
res.send({content}); // sends the md file content
});

With server components, this can be loaded at runtime, saving the client from having to download and parse 75K of libraries and wait for a second request to fetch the data after the page loads, just to render static content that will not change for the lifetime of the page.

// Libraries not included in client bundle
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
async function Page({page}) {
// NOTE: loads *during* render, when the app is built.
const content = await file.readFile(`${page}.md`);
return <div>{sanitizeHtml(marked(content))}</div>;
}

Server Actions

This is a new functionality that allows client components to call async functions executed on the server. In other words, it is a binding between the server and the client. Defined with the ‘use server’ directive, these so-called Server Actions enable frameworks to automatically create a reference to the server function and pass that reference to the Client Component. 

Server Actions can be created in Server Components and passed as props to Client Components, or they can be imported and used in Client Components.

An important thing to note here is that ‘use server’ is not a directive you would put on top of a Server Component. Instead, it just creates a link between the server and the client. For example, putting ‘use client’ on top of a file informs React to send the JavaScript to the client where it can be used i.e. a tunnel from the server to send data to the client.

With ‘use server,’ it is the same but with the opposite direction - a tunnel on the server for the client to send data to. For example, to submit data with a form ‘use server’ would be used to let the server accept the form data from the client.

An example of a Server Component to add entries to a db would be:

import Button from './Button'; // Client component
// Server Component
function EmptyNote () {
async function createNoteAction() {
// Server Action
'use server';
await db.notes.create();
}
return <Button onClick={createNoteAction}/>;
}

When React renders the EmptyNote Server Component, it will create a reference to the createNoteAction function, and pass that reference to the Button Client Component. When the button is clicked, React will send a request to the server to execute the createNoteAction function with the reference provided:

"use client";
export default function Button({onClick}) {
return <button onClick={onClick}>Create Empty Note</button>
}

In this example, the onClick Server Action is passed as a prop, but it could also be imported from a file:

// actions.js
"use server";
export async function createNoteAction() {
await db.notes.create();
}
"use client";
import {createNoteAction} from './actions';
function EmptyNote() {
<button onClick={createNoteAction} />
}

Although these functionalities are different on their own, having access to both could be very handy for developers as it would improve reusability and modularity.

Other benefits of this new addition are improved SEO—more accessible content and enhanced performance—quicker initial page loads, especially for bigger applications that require a ton of resources and substantial content loads.

Now, let’s dive into the second part of the guide, where we’ll explore React 19’s improvements and additions, and the third part, where we talk about the upgraded support in React 19.

Insights

Learn more about React

cover

Jun 24Article

Writing Custom Renderers for React

Learn how React renderers work, why RNTL needed a new test renderer, and what it takes to connect React Reconciler to a host environment.

cover

Dec 18Live Stream

Building iOS Live Activities With React: Inside Voltra

Learn how to build iOS Live Activities and Dynamic Island experiences using pure JSX, no Swift or native code required.

cover

Nov 27Podcast

From React Native Web to React Strict DOM

Nicolas Gallagher (Meta) explains how React Strict DOM evolved from React Native Web, providing a shared syntax that compiles to native or web with zero overhead.

cover

Nov 21Podcast

React Foundation: New Governance Model for React and React Native

Seth Webster, Executive Director of the React Foundation, details its new governance model, Meta's role, and how partners like Callstack and Microsoft will ensure stability and scale.

cover

Apr 9Podcast

Exploring TanStack Ecosystem: From React Query to TanStack Start

Tanner Linsley breaks down TanStack’s evolution-from React Query to Start, and why client-first React apps still matter in today’s full-stack landscape.

Event Cover

Feb 20Live Stream

Create React App is Out: Exploring Alternatives & Community Proposals

CRA is now deprecated, so we explore other options for starting web React projects, going through Vite, TanStack Router, Parcel, and Rsbuild to check the developer experience.

Event Cover

Feb 13Live Stream

State of React 2024 Breakdown

Let's take a deep dive into the long-awaited State of React survey from 2024; brace yourselves for a two-hour ride.

cover

Sep 9Article

The Complete Developer Guide to React 19, Part 3: Upgraded Support

Learn how React 19's new features enhance SEO, page performance, and custom elements support for better, faster web applications.

cover

Sep 3Article

The Complete Developer Guide to React 19, Part 2: New Enhancements

Discover React 19's key updates, including improved `ref` usage, error reporting, and context consumption in part 2 of this practical developer guide.

cover

May 2Article

Understanding React Server Components

React Server Components (RSC) are still work in progress, but now it's the best time to explore these core concepts of future React. Find out why and how!

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.