Quick Start

Forms are undoubtedly among the most commonly used fundamental building blocks on the web. Sign up, log in, subscribe, add payment method... We are constantly filling these out. So, how can we manage these critically important forms more effectively in our applications? What do the form actions introduced in React 19 offer us?


Traditional Method

The previous traditional method for handling form submissions in React required several manual steps (like creating a new useState for each input). When a user submitted a form, we typically managed the process as follows:

import { useState } from "react";

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const NewsletterForm = () => {
  const [email, setEmail] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsSubmitting(true);
    await sleep(3000);
    setIsSubmitting(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Submitting..." : "Subscribe"}
      </button>
    </form>
  );
};

export default NewsletterForm;

  1. We would create a <form> element and bind a custom function (like handleSubmit) to the onSubmit event to run when the form was submitted.
  2. Inside handleSubmit, the first step is typically to call event.preventDefault(). This prevents the browser from reloading the page.
  3. A separate state (using useState) would be defined for each input field in the form (e.g., email).
  4. An onChange event listener would be added to each input field to capture the user's input and update the corresponding state in real-time.
  5. During form submission (e.g., while an API request is in progress), we also had to manually manage an additional state, like isSubmitting, to update the user interface (e.g., disable the button, display "Submitting..." text).

React v19: useActionState

React 19 significantly simplifies form management with the useActionState hook. This hook allows us to handle form submissions, state management, and pending states with less code and in a more declarative manner.

import { useActionState } from "react";

type NewsletterFormState = {
  email: string;
};

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const NewsletterForm = () => {
  const handleSubmit = async (
    _previousState: NewsletterFormState,
    formData: FormData
  ) => {
    const email = formData.get("email");

    await sleep(3000);

    return { email: email as string };
  };

  const initialState: NewsletterFormState = { email: "" };

  const [state, formAction, isPending] = useActionState(
    handleSubmit,
    initialState
  );

  return (
    <form action={formAction}>
      {JSON.stringify(state)}
      <input name="email" type="email" />
      <button type="submit" disabled={isPending}>
        {isPending ? "Submitting..." : "Subscribe"}
      </button>
    </form>
  );
};

export default NewsletterForm;

As seen in the code above, by passing the action={formAction} prop to the <form> element, we delegate the form management mechanism to the useActionState hook. The key benefits of this approach are:

  1. Simplified State Management: There's no need to define separate useState for each input and update them with onChange. When the form is submitted, the values of inputs with a name attribute are automatically passed to the handleSubmit function via a FormData object.
  2. Automatic Pending State: The hook automatically provides an isPending boolean value which is true while the handleSubmit function is running (during await sleep(3000)). This eliminates the need to maintain an extra isSubmitting state.
  3. Less Code: The onSubmit propevent.preventDefault(), and onChange handlers for inputs are no longer needed.
  4. State Updates: The value returned by the action function (handleSubmit) (e.g., { email: ... }) is automatically captured by the hook and set as the new state value.

Thanks.