DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

10 - React: Form Actions

Form actions are a newer React pattern for handling form submissions with server-style action functions.

The basic idea is simple: instead of wiring a form to onSubmit and manually calling preventDefault(), we give the form an action.

async function saveUser(formData: FormData) {
  const username = formData.get('username');
  console.log(username);
}

export default function FormExample() {
  return (
    <form action={saveUser}>
      <input name="username" />
      <button type="submit">Save</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

In a React form action:

  • we typically do not call preventDefault() manually
  • the browser passes a FormData object to the action
  • the form is often reset after a successful submit

1) Basic form action behavior

A form action is useful because it gives us a more declarative submit flow.

For example:

async function submitForm(formData: FormData) {
  const username = formData.get('username');

  if (!username || typeof username !== 'string') {
    return { error: 'Username is required' };
  }

  return { success: true };
}
Enter fullscreen mode Exit fullscreen mode

The handler receives the form data directly, and we can process it without writing custom submit event logic.

2) useActionState

useActionState is a hook for managing the state of a form action.

It helps when we want to:

  • keep state across submissions
  • return validation errors
  • know whether the action is pending
  • display form feedback after submission
import { useActionState } from 'react';

async function submitForm(prevState: { error: string | null }, formData: FormData) {
  const username = formData.get('username');

  if (!username || typeof username !== 'string' || username.trim().length < 3) {
    return { error: 'Username must be at least 3 characters long' };
  }

  return { error: null };
}

export default function FormExample() {
  const [state, formAction, isPending] = useActionState(submitForm, { error: null });

  return (
    <form action={formAction}>
      <input name="username" />
      <button disabled={isPending} type="submit">
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      {state.error && <p>{state.error}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

The hook returns:

  • the current state
  • a wrapped action function to pass to the form
  • a boolean telling us whether the action is pending

Important note

The action function signature is different from a normal submit handler:

function actionHandler(previousState, formData) {
  // validate and return a new state
}
Enter fullscreen mode Exit fullscreen mode

The previous state is available so the action can react to earlier values.

3) Keeping input values after submit

When the form resets after a successful submit, the UI may lose the typed values.

If we want to keep the values visible, we can include them in the returned state and use them as default values.

import { useActionState } from 'react';

type FormState = {
  error: string | null;
  username: string;
};

async function submitForm(prevState: FormState, formData: FormData): Promise<FormState> {
  const username = String(formData.get('username') ?? '');

  if (username.trim().length < 3) {
    return {
      error: 'Username must be at least 3 characters long',
      username,
    };
  }

  return {
    error: null,
    username,
  };
}

export default function FormExample() {
  const [state, formAction] = useActionState(submitForm, {
    error: null,
    username: '',
  });

  return (
    <form action={formAction}>
      <input name="username" defaultValue={state.username} />
      {state.error && <p>{state.error}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This pattern is useful when the form should preserve previous input values even after validation fails or the form resets.

4) useFormStatus

useFormStatus is used to read the status of the current form submission.

Important: it only works in a component that is rendered inside a form.

import { useFormStatus } from 'react';

function SubmitButton() {
  const { pending } = useFormStatus();

  return <button disabled={pending}>{pending ? 'Submitting...' : 'Send'}</button>;
}

export default function FormExample() {
  return (
    <form action={async (formData) => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log(formData.get('username'));
    }}>
      <input name="username" />
      <SubmitButton />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

pending becomes true while the action is running, so the button can be disabled or show loading text.

5) Multiple actions on one form

Form actions are not limited to only one submit button. We can attach different actions to different buttons.

function likeAction(formData: FormData) {
  console.log('liked');
}

function dislikeAction(formData: FormData) {
  console.log('disliked');
}

export default function VoteForm() {
  return (
    <form>
      <button type="submit" formAction={likeAction}>Upvote</button>
      <button type="submit" formAction={dislikeAction}>Downvote</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is useful when a form can trigger different actions depending on which control submits it.

6) Summary

  • form actions are a React pattern for declarative submission handling
  • useActionState manages state and pending status for form actions
  • the action receives FormData
  • actions can return validation state or other UI state
  • useFormStatus lets a nested button know whether a form is currently submitting
  • multiple submit buttons can trigger different actions with formAction

This pattern is especially useful in newer React apps when you want a more server-action-like form flow.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.