Take a look at the useActionState, useFormStatus and useOptimistic Hooks, new in React 19.
React 19 has recently emerged as a powerful evolution in the React ecosystem, bringing with it a suite of new React Hooks designed to simplify state management, enhance asynchronous operations and optimize performance. In this guide, we’ll dive deep into these new additions: useActionState, useFormStatus and useOptimistic.
One of the most notable additions in React 19 is the useActionState Hook, a powerful tool that simplifies the often repetitive task of managing form state and asynchronous updates. In earlier versions of React, developers typically had to juggle multiple useState calls to manage form data, loading states and errors. The useActionState
Hook abstracts these concerns into a single, cohesive API.
The useActionState
Hook is designed to handle the entire lifecycle of a form action, from triggering the action to managing the form’s state during and after the submission. It takes three parameters:
The useActionState
Hook returns a tuple with three elements:
import { useActionState } from "react";
export function Component() {
const [state, dispatch, isPending] =
useActionState(
action,
initialState,
permalink,
);
// ...
}
Let’s walk through a practical example to illustrate how useActionState
can be applied in a real-world scenario. Assume we wanted to build a simple feedback form that allows users to submit feedback asynchronously.
import { useActionState } from "react";
export function FeedbackForm() {
// ...
}
To begin building our feedback form, we’ll first define the action function that will handle the form submission. This function is central to the useActionState
Hook, as it manages the core logic for processing the form data and determining what state should be returned based on the success or failure of the submission.
Before we do that, we’ll simulate an API call using a simple function named submitToAPI()
. This function mimics the behavior of sending form data to a server, which, in real-world applications, might involve making a Fetch or Axios call to an external endpoint.
const submitToAPI = async (formData) => {
// Simulating an API call
await new Promise((resolve) =>
setTimeout(resolve, 1000),
);
return `Submitted: ${formData.get("text")}`;
};
The submitToAPI()
function accepts a formData
, a FormData object, and processes it asynchronously by simulating a network delay, and then returns a success message. This return value will be used to update the form’s state after submission.
Next, we define the action function itself. The action function is responsible for handling the logic that occurs when the form is submitted. It’s where we’ll call submitToAPI()
and manage the different outcomes—whether the submission is successful or fails due to an error.
const submitToAPI = async (formData) => {
// Simulating an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
return `Submitted: ${formData.get('text')}`;
};
const action = async (currentState, formData) => {
try {
const result = await submitToAPI(formData);
return { message: result, error: null };
} catch (error) {
return { message: null, error: "Failed to submit form" };
}
};
In the above action
function, we’re handling two primary scenarios. If the submitToAPI
call is successful, the function returns an object containing the success message and a null
value for the error.
Conversely, if an error occurs during the submission (perhaps due to a network issue or server error), the function returns an object with a null
message and an appropriate error message. This design allows the form component to react accordingly, displaying either a success message or an error message to the user.
Now that we have our action
function and API simulation in place, it’s time to integrate everything into our form component using useActionState
. The Hook will take our action function and an initial state object and handle the form’s state management throughout its lifecycle.
import { useActionState } from "react";
const submitToAPI = async (formData) => {
// Simulating an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
return `Submitted: ${formData.get('text')}`;
};
const action = async (currentState, formData) => {
try {
const result = await submitToAPI(formData);
return { message: result, error: null };
} catch (error) {
return { message: null, error: "Failed to submit form" };
}
};
export function FeedbackForm() {
const [state, dispatch, isPending] = useActionState(
action,
{ message: null, error: null }
);
return (
<form action={dispatch}>
<input type="text" name="text" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit Feedback'}
</button>
{state.message && <p className="success">{state.message}</p>}
{state.error && <p className="error">{state.error}</p>}
</form>
);
}
Here’s a breakdown of what happens within the FeedbackForm
component:
State Management with useActionState: We initialize the Hook with the action
function and an initial state object that includes placeholders for message
and error
. This setup prepares the form to handle both success and error states, depending on the outcome of the submission.
Form Submission with dispatch: The dispatch
function, returned by useActionState
, is assigned to the form’s action
attribute. When the user submits the form, dispatch
triggers the action
function, initiating the form submission process.
Handling the pending state: The isPending
boolean indicates whether the form is currently in the process of submitting data. If isPending
is true
, the form inputs and the submit button are disabled, preventing the user from making additional useActionState
submissions or interacting with the form until the current action is completed. This provides a smoother user experience and prevents potential issues from multiple submissions.
Displaying feedback: After the form is submitted, the component checks the state
object for either a success message or an error message. If a message is present, it is displayed to the user. If there’s an error, the error message is shown instead. This dynamic feedback mechanism keeps users always informed about the status of their submission, whether it succeeded or failed.
With these changes, here’s how our app appears when data is submitted successfully in the FeedbackForm
component.
useActionState
drastically reduces the complexity of managing forms in React.Another valuable addition in React 19 is the useFormStatus Hook, which allows nested child components to access information about their parent form without relying on prop drilling or context. This is particularly useful in complex forms where different components need to respond to the form’s submission status or data.
In many complex forms, different sections or fields are often managed by their own components. For instance, you might have a form where personal information, billing details and shipping addresses are each managed by distinct components. Managing the form’s overall state, such as whether it is currently submitting or whether there’s an error, can become cumbersome when this state needs to be passed down through multiple layers of components.
This is where useFormStatus
shines. It allows any component within the form hierarchy to easily access and respond to the form’s current state, such as whether the form is pending submission, has encountered an error or has been successfully submitted.
The useFormStatus
Hook does not take any parameters and returns an object containing the following properties:
POST
or GET
.import { useFormStatus } from "react";
export function NestedComponent() {
/* access form information */
const { pending, data, method, action } = useFormStatus();
return (
/* template */
);
}
Let’s explore how useFormStatus
can be used in a nested component to enhance a form’s functionality.
import { useFormStatus } from "react";
function SubmitButton() {
const { pending, data } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
{data && (
<span className="data-preview">
{data.get("text")}
</span>
)}
</button>
);
}
export function FeedbackForm() {
return (
<form action="/submit-feedback">
<input type="text" name="text" />
<SubmitButton />
</form>
);
}
In the above example, the SubmitButton
component accesses the form’s status using useFormStatus
. It automatically disables itself when the form is in the process of being submitted (pending
state) and can display a preview of the data being submitted. This allows for a more dynamic and responsive user interface, where nested components can independently respond to changes in the form’s state.
useFormStatus
Hook, we eliminate the need to pass props through multiple component layers, simplifying your component structure.useFormStatus
automatically re-render when the form’s status changes, ensuring the UI always reflects the current state of the form.Optimistic updates are a powerful technique in web development that can significantly improve the perceived performance and responsiveness of your application. React 19’s useOptimistic Hook provides a simple yet effective way to implement optimistic updates, allowing you to update the UI immediately while a background operation, such as a network request, is still ongoing.
The useOptimistic
Hook is designed to handle scenarios where you want to display an immediate UI update before an operation is fully completed. It takes two arguments:
The Hook returns an array with two elements:
import { useOptimistic } from "react";
export function Component() {
const [optValue, setOptValue] = useOptimistic(
currentValue,
optUpdateFunction,
);
return (
/* template */
);
}
First, let’s implement a basic todo list without using the useOptimistic
Hook. In this version, a new todo item will appear only after the API call is completed.
import { useState } from "react";
const addTodoToAPI = async (todo) => {
// Simulating an API call
await new Promise((resolve) => setTimeout(resolve, 1000));
return { id: Date.now(), text: todo };
};
export function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = async (formData) => {
const todo = formData.get("todo");
const newTodo = await addTodoToAPI(todo);
setTodos((currentTodos) => [...currentTodos, newTodo]);
};
return (
<div>
<form action={addTodo}>
<input type="text" name="todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
In the above implementation, the todo list component uses the useState Hook to manage the list of todos and an addTodo()
function to handle the form submission. When the form is submitted, the addTodoToAPI()
function simulates an API call to add the todo item. After the API call is completed, the new todo item is added to the state, and the UI is updated to reflect the change. This approach ensures that the new todo item is only displayed after the API call is successful, providing a reliable, albeit slightly delayed, user experience.
Now, we’ll enhance the todo list by using the useOptimistic
Hook to provide immediate feedback when a new todo item is added.
import { useOptimistic, useState } from "react";
const addTodoToAPI = async (todo) => {
// Simulating an API call
await new Promise((resolve) =>
setTimeout(resolve, 1000),
);
return { id: Date.now(), text: todo };
};
export function TodoList() {
const [todos, setTodos] = useState([]);
const [optimisticTodos, addOptimisticTodo] =
useOptimistic(
todos,
(currentTodos, newTodo) => [
...currentTodos,
{
id: "temp",
text: newTodo,
pending: true,
},
],
);
const addTodo = async (formData) => {
const todo = formData.get("todo");
addOptimisticTodo(todo);
const newTodo = await addTodoToAPI(todo);
setTodos((currentTodos) => [
...currentTodos,
newTodo,
]);
};
return (
<div>
<form action={addTodo}>
<input type="text" name="todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li
key={todo.id}
style={{
opacity: todo.pending ? 0.5 : 1,
}}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
In this enhanced version, the useOptimistic
Hook allows the TodoList
component to display new todo items immediately after the user submits the form, even before the API call completes. The addOptimisticTodo()
function adds a temporary, “optimistic” todo item to the list with reduced opacity to indicate that the addition is pending. Once the API call completes successfully, the actual todo item replaces the optimistic one, and the UI is updated accordingly.
This approach provides a smoother and more responsive user experience by allowing users to see their actions reflected in the UI immediately.
useOptimistic
enhances the responsiveness of the application, making it feel faster and more fluid to the user.React 19 introduces a suite of powerful new Hooks that significantly enhance the developer experience, particularly in managing state, form handling and UI updates.
The useActionState
Hook streamlines form state management by consolidating multiple state updates into a single, easy-to-use API. This not only simplifies your code but also improves the user experience by providing seamless form submissions with built-in async handling and automatic UI updates.
The useFormStatus
Hook is helpful for complex forms, allowing child components to access and respond to the form’s state without the need for cumbersome prop drilling. This results in more modular and reusable components, making it easier to build and maintain large-scale forms in your applications.
Finally, the useOptimistic
Hook brings the power of optimistic UI updates by allowing us to update the UI immediately while background operations are still in progress.
For a deep-dive into a new React 19 API that isn’t a Hook, check out this article on the
use()
API—React Context with the New Use API.
Whether you’re managing complex forms, optimizing user interactions or just looking to improve the maintainability of your React components, these new additions to React 19 will become indispensable tools in our development arsenal. For more details, be sure to check out the section “What’s new in React 19” in the official React documentation.
Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.