Skip to content

Form Actions — Advanced Form Handling with Server Actions

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Form Actions. We cover key concepts, practical examples, and best practices to help you master this topic.

Form Actions combine HTML form semantics with Server Actions to handle form submissions, validation, pending states, and error handling in a unified pattern.

What You'll Learn

You will understand how to build forms with Server Actions, handle validation errors, manage pending states, implement optimistic updates, and combine forms with client-side interactivity.

Why It Matters

Form Actions reduce boilerplate by handling the entire form lifecycle on the server while keeping the form interactive and responsive on the client.

Real-World Use

Durga Antivirus Pro's threat reporting form uses Form Actions to submit threat data, validate fields on the server, revalidate the threat dashboard, and return structured error messages.

flowchart TD
    A[User Fills Form] --> B[Form Submit]
    B --> C{Client Validation}
    C -->|Pass| D[Server Action]
    C -->|Fail| E[Show Client Errors]
    D --> F{Server Validation}
    F -->|Pass| G[Database Mutation]
    F -->|Fail| H[Return Field Errors]
    G --> I[Revalidate & Redirect]
    H --> E
    style D fill:#1e293b,color:#fff
    style G fill:#0f172a,color:#fff

Basic Form with Server Action

The simplest form action pattern passes the Server Action function directly to the form element.

async function subscribeToNewsletter(formData) {
  'use server';
  const email = formData.get('email');
  if (!email || !email.includes('@')) {
    return { error: 'Please provide a valid email address' };
  }
  await db.newsletter.create({ email });
  revalidatePath('/newsletter');
  return { success: true };
}

export default function NewsletterForm() {
  return (
    <form action={subscribeToNewsletter}>
      <input type="email" name="email" placeholder="your@email.com" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

Expected output: A newsletter subscription form that validates the email on the server, saves it to the database, and revalidates the page. The form works without JavaScript.

Returning Structured Errors

Return field-level errors from Server Actions to display specific validation messages next to each input.

async function registerUser(prevState, formData) {
  'use server';
  const errors = {};
  const name = formData.get('name');
  const email = formData.get('email');
  const password = formData.get('password');

  if (!name || name.length < 2) errors.name = 'Name must be at least 2 characters';
  if (!email || !email.includes('@')) errors.email = 'Invalid email address';
  if (!password || password.length < 8) errors.password = 'Password must be at least 8 characters';

  if (Object.keys(errors).length > 0) {
    return { errors };
  }

  await db.users.create({ name, email, password: hash(password) });
  revalidatePath('/users');
  return { success: true };
}

function RegisterForm() {
  const [state, formAction, pending] = useActionState(registerUser, { errors: {} });

  return (
    <form action={formAction}>
      <div>
        <input name="name" placeholder="Name" />
        {state.errors?.name && <p style={{ color: 'red' }}>{state.errors.name}</p>}
      </div>
      <div>
        <input name="email" type="email" placeholder="Email" />
        {state.errors?.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
      </div>
      <div>
        <input name="password" type="password" placeholder="Password" />
        {state.errors?.password && <p style={{ color: 'red' }}>{state.errors.password}</p>}
      </div>
      <button type="submit" disabled={pending}>
        {pending ? 'Registering...' : 'Register'}
      </button>
      {state.success && <p style={{ color: 'green' }}>Registration successful!</p>}
    </form>
  );
}

Expected output: A registration form with individual error messages for each field. The button shows a pending state during submission. Field errors display below the relevant input.

Optimistic Updates with useOptimistic

React 19's useOptimistic hook lets you update the UI immediately while the Server Action processes in the background.

'use client';
import { useOptimistic, useActionState } from 'react';
import { addComment } from './actions';

function CommentSection({ initialComments, postId }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    initialComments,
    (state, newComment) => [...state, { ...newComment, pending: true }]
  );

  const [state, formAction, pending] = useActionState(
    async (prevState, formData) => {
      const text = formData.get('text');
      addOptimisticComment({ text, id: 'temp', author: 'You' });
      return await addComment(prevState, formData);
    },
    { error: null }
  );

  return (
    <div>
      <form action={formAction}>
        <textarea name="text" required />
        <button type="submit" disabled={pending}>Post</button>
      </form>
      <ul>
        {optimisticComments.map(c => (
          <li key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>
            <strong>{c.author}</strong>: {c.text}
          </li>
        ))}
      </ul>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
    </div>
  );
}

Expected output: When the user submits a comment, it appears immediately in the list with reduced opacity. Once the Server Action completes, the list updates with the real comment data. If it fails, the optimistic comment is removed and an error displays.

Combining Form Actions with Client-Side Validation

Add client-side validation for instant feedback while keeping server validation for security.

'use client';
function ContactForm() {
  const [errors, setErrors] = useState({});
  const [state, formAction, pending] = useActionState(sendMessage, { error: null });

  function validate(formData) {
    const newErrors = {};
    if (!formData.get('name')) newErrors.name = 'Name is required';
    if (!formData.get('email')?.includes('@')) newErrors.email = 'Valid email required';
    if (!formData.get('message') || formData.get('message').length < 10) {
      newErrors.message = 'Message must be at least 10 characters';
    }
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }

  async function handleSubmit(formData) {
    if (!validate(formData)) return;
    return await sendMessage(null, formData);
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="Name" />
      {errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
      <input name="email" type="email" placeholder="Email" />
      {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      <textarea name="message" placeholder="Message" />
      {errors.message && <p style={{ color: 'red' }}>{errors.message}</p>}
      <button type="submit" disabled={pending}>
        {pending ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

Expected output: The form validates fields immediately on submission with client-side checks. If valid, it sends the data to the server. Server-side validation catches any remaining issues.

Common Mistakes

  1. Not using useActionState for form state: Without useActionState, you lose access to the return value of Server Actions. Always wrap Server Actions with useActionState for form feedback.

  2. Relying only on client-side validation: Client validation can be bypassed. Always validate on the server inside the Server Action.

  3. Forgetting to handle the pending state: Without pending state, users can submit the form multiple times. Disable the submit button while the action is pending.

  4. Not resetting the form after success: After successful submission, clear the form or show a success message. Users should know the action completed.

  5. Making forms dependent on JavaScript: Use the HTML form action attribute so the form works without JS. JavaScript should enhance, not be required.

Practice Questions

  1. How do you access form data inside a Server Action?

Use the formData.get('fieldName') method on the FormData object passed to the action.

  1. What hook manages form state and pending status in React 19?

useActionState. It returns the current state, a form action function, and a pending boolean.

  1. How do you show field-specific validation errors from a Server Action?

Return an object with an errors property containing field-specific messages. Render them next to each input in the form.

  1. What is the benefit of useOptimistic with Server Actions?

It updates the UI immediately before the server responds, making the app feel faster. The UI corrects automatically if the server action fails.

  1. Can a form with a Server Action work without JavaScript?

Yes, if the form uses the action attribute directly with a Server Action function. The browser submits the form natively.

Challenge

Build a todo list with an add form that uses optimistic updates. The todo appears immediately, and if the Server Action fails, it reverts with an error message. Include a checkbox to toggle completion status.

Frequently Asked Questions

Can I use Form Actions with file uploads?

Yes, but the FormData includes the file. For large files, consider using dedicated upload endpoints with progress tracking.

How do I redirect after a form action?

Use the redirect function from next/navigation inside the Server Action after the mutation succeeds.

Can I call multiple Server Actions from one form?

No. A form can only have one action attribute. If you need different actions, use separate forms or a single action that handles all cases.

Do Form Actions support GET submissions?

Server Actions work with POST submissions. For search forms that need GET, use a standard form with action pointing to a route handler.

How do I reset a form after a successful Server Action?

Use a key prop on the form that changes after success, or manually reset the form elements using the useActionState return value.

Mini Project

Build a product review form that validates fields (rating 1-5, comment required), handles errors per field, uses optimistic updates to show the review immediately, and revalidates the product page.

What's Next

Learn about the use server Directive in detail, including how to organize Server Actions in separate files.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro