State Feedback with useActionState and useOptimistic
Executing server tasks in the background is great, but users expect visual feedback. They need to see validation errors, loading indicators, and immediate results. Let us explore two hooks: useActionState and useOptimistic.
1. Handling Action State with useActionState
The useActionState hook (previously named useFormState in React 18) allows you to capture response values (such as success confirmation messages or form validation errors) returned by a Server Action.
First, update your Server Action to return a state response:
// src/app/actions.js
"use server";
export async function joinNewsletter(prevState, formData) {
const email = formData.get("email");
if (!email || !email.includes("@")) {
return { success: false, message: "Please enter a valid email address." };
}
// Save to database
return { success: true, message: "Subscription success! Welcome aboard." };
}Next, consume the action inside a Client Component:
// src/app/NewsletterForm.jsx
"use client";
import { useActionState } from "react";
import { joinNewsletter } from "./actions";
export default function NewsletterForm() {
// Hook returns: [current state response, form action wrapper, transition pending boolean]
const [state, formAction, isPending] = useActionState(joinNewsletter, {
success: false,
message: ""
});
return (
<form action={formAction} className="max-w-sm mt-4">
<input name="email" type="email" placeholder="email@example.com" required className="border p-2" />
<button type="submit" disabled={isPending} className="ml-2 bg-blue-600 text-white p-2">
{isPending ? "Subscribing..." : "Subscribe"}
</button>
{state.message && (
<p className={state.success ? "text-green-600 mt-2" : "text-red-600 mt-2"}>
{state.message}
</p>
)}
</form>
);
}2. Implementing useOptimistic Updates
In slow network environments, waiting for database confirmation can make the UI feel sluggish. Optimistic Updates allow you to update the UI instantly under the assumption that the server request will succeed, reverting the UI state only if the request fails.
Here is how to implement this using useOptimistic:
// src/app/TodoApp.jsx
"use client";
import { useOptimistic, useState } from "react";
import { addTodoAction } from "./actions";
export default function TodoApp({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
// Hook returns: [optimistic state representation, state modifier callback]
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodoTitle) => [
...state,
{ id: Date.now(), title: newTodoTitle, pending: true } // Add temporary item
]
);
const handleSubmit = async (formData) => {
const title = formData.get("title");
if (!title) return;
// 1. Instantly trigger optimistic state update
addOptimisticTodo(title);
// 2. Execute actual server task
const updatedTodos = await addTodoAction(title);
// 3. Update actual state (causes optimistic state to resolve)
setTodos(updatedTodos);
};
return (
<div>
<form action={handleSubmit}>
<input name="title" required className="border p-2" />
<button type="submit" className="bg-blue-600 text-white p-2">Add</button>
</form>
<ul className="mt-4">
{optimisticTodos.map((todo) => (
<li key={todo.id} className={todo.pending ? "opacity-50" : ""}>
{todo.title} {todo.pending && "(saving...)"}
</li>
))}
</ul>
</div>
);
}