React 19 Server Actions and Forms: Complete Production Guide

React 19 Server Actions in Production

React 19 server actions fundamentally change how we handle forms and data mutations in React applications. Instead of writing API routes, fetch calls, loading states, and error handling separately, server actions let you define server-side functions that can be called directly from your components.

This guide covers the practical patterns for using server actions in production — from basic form handling to optimistic updates, progressive enhancement, and error boundaries. By the end, you will know when server actions simplify your code and when traditional API routes are still the better choice.

Understanding Server Actions

A server action is an async function marked with the “use server” directive. It runs exclusively on the server but can be called from client components as if it were a local function. React handles the network request, serialization, and state management automatically.

// app/actions/posts.ts
"use server";

import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  category: z.enum(["tech", "design", "business"]),
});

export async function createPost(prevState: ActionState, formData: FormData) {
  // Validate input
  const parsed = CreatePostSchema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
    category: formData.get("category"),
  });

  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.flatten().fieldErrors,
    };
  }

  // Server-side logic — database, auth, etc.
  try {
    const post = await db.posts.create({
      data: {
        ...parsed.data,
        authorId: await getCurrentUserId(),
        publishedAt: new Date(),
      },
    });

    revalidatePath("/posts");
    return { success: true, postId: post.id };
  } catch (error) {
    return { success: false, errors: { _form: ["Failed to create post"] } };
  }
}
React 19 server actions development
Server actions eliminate boilerplate for form handling in React applications

useActionState: The New Standard

React 19 introduces useActionState (replacing the experimental useFormState) as the primary hook for working with server actions. It manages pending state, return values, and progressive enhancement:

// app/components/CreatePostForm.tsx
"use client";

import { useActionState, useOptimistic } from "react";
import { createPost } from "@/app/actions/posts";

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, {
    success: false,
    errors: {},
  });

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          name="title"
          required
          disabled={isPending}
          aria-describedby={state.errors?.title ? "title-error" : undefined}
        />
        {state.errors?.title && (
          <p id="title-error" className="text-red-500">
            {state.errors.title[0]}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea id="content" name="content" required disabled={isPending} />
        {state.errors?.content && (
          <p className="text-red-500">{state.errors.content[0]}</p>
        )}
      </div>

      <select name="category" required>
        <option value="tech">Tech</option>
        <option value="design">Design</option>
        <option value="business">Business</option>
      </select>

      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create Post"}
      </button>

      {state.errors?._form && (
        <p className="text-red-500">{state.errors._form[0]}</p>
      )}
    </form>
  );
}

Optimistic Updates with useOptimistic

For the best user experience, show the result immediately while the server action runs in the background. React 19’s useOptimistic hook makes this straightforward:

// Optimistic todo list
"use client";

import { useOptimistic } from "react";
import { toggleTodo } from "@/app/actions/todos";

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, setOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, toggledId: string) =>
      currentTodos.map((todo) =>
        todo.id === toggledId
          ? { ...todo, completed: !todo.completed }
          : todo
      )
  );

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id}>
          <form
            action={async () => {
              setOptimisticTodo(todo.id); // Instant UI update
              await toggleTodo(todo.id);  // Server action in background
            }}
          >
            <button type="submit">
              {todo.completed ? "✓" : "○"} {todo.title}
            </button>
          </form>
        </li>
      ))}
    </ul>
  );
}
React form development with server actions
Optimistic updates create instant-feeling interactions in React 19 apps

Progressive Enhancement

One of the most powerful features of React 19 server actions is progressive enhancement. Forms using server actions work even before JavaScript loads. The form submits as a standard HTML form, the server action processes it, and the page re-renders with the result.

// This form works WITHOUT JavaScript loaded
// useActionState provides the progressive enhancement
export function SearchForm() {
  const [state, action, isPending] = useActionState(searchPosts, {
    results: [],
    query: "",
  });

  return (
    <form action={action}>
      <input
        name="query"
        defaultValue={state.query}
        placeholder="Search posts..."
      />
      <button type="submit">Search</button>

      {state.results.map((post) => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </form>
  );
}

When NOT to Use Server Actions

Server actions are not a replacement for all API routes. Additionally, avoid them for: real-time data (WebSockets/SSE are better), file uploads over 50MB (use presigned URLs), third-party webhook endpoints, or when you need fine-grained HTTP control (custom headers, status codes, streaming responses).

Modern web development patterns with React
Choosing between server actions and API routes based on use case requirements

Key Takeaways

React 19 server actions eliminate the boilerplate of form handling — no manual fetch calls, no loading state management, no separate API routes for simple mutations. Combined with useActionState and useOptimistic, they provide a complete solution for data mutations. As a result, start using them for forms and simple mutations, but keep API routes for complex scenarios.

Related Reading

External Resources

Scroll to Top