React 19 has introduced some exciting new hooks that aim to streamline state management and enhance user interaction workflows. The new hooks โ useActionState, useFormStatus, and useOptimistic โ allow developers to handle various aspects of UI state in a more declarative and efficient way.
In this blog, we will explore what these hooks do, how to use them, and how they simplify common patterns in modern React development.
1. useActionState: Managing the Lifecycle of Actions
One of the challenges in UI development is managing the state transitions that occur during an action’s lifecycle. Whether it’s a network request, an animation, or any kind of async operation, keeping track of the status and result of the action is vital.
The useActionState hook makes this easy by giving us a declarative way to track the states of an action.
How useActionState Works:
- Action Function: You define an action function (e.g., incrementCounter) that takes two parameters:
- previousState: The current state value before the action is executed.
- formData: The data from the form or event that triggered the action.
- Initial State: You provide an initial state when you call useActionState, which sets the starting value for the state managed by the hook.
- Progressive Enhancement: If you use useActionState within a server-rendered component, the form can be submitted without needing JavaScript, enhancing performance and usability.
const [state, formAction, isPending] = useActionState(fn, initialState,
permalink?);
Basic Usage:
import { useActionState } from "react";
async function incrementCounter(previousState, formData) {
return previousState + 1;
}
export default function App() {
const [state, formAction] = useActionState(incrementCounter, 0);
return (
<form>
{state}
<button formAction={formAction}>Increment</button>
</form>
);
}
Breakdown:
โ incrementCounter: A function that takes the previous state (counter value) and
returns the incremented value.
โ useActionState: This hook manages the state (initially 0), and when the button is
clicked, it triggers the action (incrementCounter) to update the count.
โ formAction: Automatically binds the action to the button, making it simple to handle
the state change on click.
Server Action Example: Feedback Form with Loading State
- Server-side Action (submitFeedback.js):
This function simulates submitting feedback to the server and returning a success
message.
"use server";
export default async function submitFeedback(prevState, data) {
// Simulate a server-side delay (e.g., sending the feedback)
return new Promise((resolve) =>
setTimeout(() => resolve("Thank you for your feedback!"), 3000)
);
}
2. Client-side Form (FeedbackForm.js):
The client-side component handles form submission using useActionState, where
the form submission interacts with the server.
// FeedbackForm.js
"use client";
import submitFeedback from "./submitFeedback";
import { useActionState } from "react";
function FeedbackForm() {
// Initialize useActionState with the submitFeedback action and an initial
state
const [message, submitAction, isPending] = useActionState(
submitFeedback,
"No feedback yet."
);
return (
<>
<p>Status: {message}</p>
<form action={submitAction}>
<textarea
name="feedback"
placeholder="Enter your feedback here..."
rows="5"
required
className="border w-full p-2"
></textarea>
<button
type="submit"
disabled={isPending}
className="mt-2 border bg-blue-500 text-white p-2 disabled:opacity-50
disabled:cursor-not-allowed"
>
{isPending ? "Submitting..." : "Submit Feedback"}
</button>
</form>
</>
);
}
export default FeedbackForm;
2. useOptimistic: Optimistic UI Updates Simplified
useOptimistic is a powerful React hook designed to enhance user experience by providing immediate feedback during asynchronous operations. This hook allows you to display an “optimistic” state while a network request or other async action is pending, helping to create a seamless and responsive UI.
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
How useOptimistic Works:
- Initial State: The hook takes an initial state as its first argument.
- Optimistic Updates: You provide an updateFn to define how the state should change optimistically.
- Returning Optimistic State: The updateFn merges the currentState with the optimisticValue and returns the resulting optimistic state, which is used while the async action is ongoing.
- Immediate Feedback: This allows your UI to reflect the expected outcome of an action immediately, rather than waiting for the action to complete.
Basic Usage:
App.js
import { useOptimistic, useState, useRef } from "react";
function Thread({ tasks, sendTasks }) {
const formRef = useRef();
async function formAction(formData) {
addOptimisticTask(formData.get("task")); // Optimistically add the new task
formRef.current.reset();
await sendTasks(formData); // Send the message to the server
}
const [optimisticTasks, addOptimisticTask] = useOptimistic(tasks, (state, newTask) => [
...state,
{ text: newTask, sending: true }
]);
return (
<>
{optimisticTasks.map((task, index) => (
<div key={index}>
{task.text}
{!!task.sending && <small> (Adding...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="task" placeholder="Enter Your task" />
<button type="submit">Add</button>
</form>
</>
);
}
export default function App() {
const [tasks, setTasks] = useState([
{ text: "Hello there!", sending: false, key: 1 },
]);
async function sendTasks(data) {
const sentTask = await deliverTasks(data.get("task")); // Simulating a
server response
setTasks((tasks) => [...tasks, { text: sentTask }]); // Update messages
with the sent message
}
return <Thread tasks={tasks} sendTasks={sendTasks} />;
}
actions.js:
export async function deliverTasks(task) {
await new Promise((res) => setTimeout(res, 1000));
return task;
}
Breakdown:
Optimistic Updates: Optimistic updates allow the UI to update immediately, assuming the operation (like adding a task) will succeed. In our application, we achieved this using the useOptimistic hook.
Message Delivery: The function deliver Message simulates adding a task to a server.
This function can be replaced with a real API call in a production application.
State Management: The App component maintains the messages state, which stores an array of tasks objects. Initially, it includes a welcome message.
Task Adding: The sendMessage function is responsible for adding the task. It simulates a delay (as if waiting for a server response) and updates the task list with the new task once the server responds.
Conclusion:
In this blog post, we’ve explored two powerful React hooks introduced in React 19: useActionState and useOptimistic. These hooks significantly enhance user experience by enabling developers to create responsive and interactive applications that handle asynchronous actions more gracefully.
- useActionState allows us to manage the state of actions in a concise and structured manner. By utilizing this hook, developers can easily track the status of an action(pending, success, or error), which simplifies the logic for rendering UI components based on these states. This leads to a more intuitive user experience, as users receive immediate feedback based on their interactions with the application.
- useOptimistic takes user experience a step further by allowing applications to optimistically update the UI before an asynchronous operation completes. By rendering a temporary state that reflects the expected outcome, developers can create a seamless and responsive interface that keeps users engaged, even while waiting for server responses. This technique reduces perceived latency, making the application feel faster and more reliable.
Both hooks encourage developers to write cleaner, more maintainable code by encapsulating complex state management logic, allowing them to focus on building engaging user interfaces. By incorporating useActionState and useOptimistic into your applications, you can significantly enhance the responsiveness and overall user experience, creating applications that feel more fluid and intuitive.