Back to roadmaps nextjs Course

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>
  );
}
Published on Last updated: