DEV Community

Bharat Soni
Bharat Soni

Posted on

Level Up Your Forms with React 19's useActionState Hook

The Back Story

I've been writing about forms and data management in React for a long time. As many of us know, handling forms in React has always been a pain. You have to manage state, pass it to inputs, handle change events, and then gather all that state to use your form data. Even for something as simple as a text field, you need a state variable and a change handler.

Sure, we've gotten used to this pattern — but honestly, it's a lot more work compared to plain HTML forms.


Enter React 19

Recently, React launched a new stable version that introduced several new hooks. Among the updates, one that stands out is the useActionState hook. And it's a game changer — especially for forms.

With useActionState, you no longer need to manually manage state or write a change handler just to get the value of a text field.

Let’s look at a simple example: a user details form. It has a few input fields, and when the user clicks the submit button, we validate the input and send the data to the server.

export default function Form() {
  return (
    <div className="form-cont">
      <form>
        <input
          type="text"
          placeholder="Username"
          name="user"
          aria-label="Username"
        />

        <input
          type="password"
          placeholder="Password"
          name="password"
          aria-label="Password"
        />

        <select name="select" defaultValue="">
          <option value="" disabled>
            -- Select a fruit --
          </option>
          <option value="banana">Banana</option>
          <option value="apple">Apple</option>
          <option value="orange">Orange</option>
        </select>

        <fieldset>
          <legend>Choose a fruit (radio)</legend>
          <label>
            <input type="radio" name="fruit" value="orange" />
            Orange
          </label>
          <label>
            <input type="radio" name="fruit" value="apple" />
            Apple
          </label>
          <label>
            <input type="radio" name="fruit" value="banana" />
            Banana
          </label>
        </fieldset>

        <fieldset>
          <legend>Select fruits (checkbox)</legend>
          <label>
            <input type="checkbox" name="fruits" value="orange" />
            Orange
          </label>
          <label>
            <input type="checkbox" name="fruits" value="apple" />
            Apple
          </label>
          <label>
            <input type="checkbox" name="fruits" value="banana" />
            Banana
          </label>
        </fieldset>

        <button type="submit">Submit</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, if you want to access the field values, you typically create state and change handlers for each input, like user, select, fruit, and fruits. I know the field names are a bit simplistic, but let's focus on the core idea.

Accessing field values is still manageable, sure — but there's a cost: each state change triggers a re-render. That can add up, especially with large forms.

This is exactly where the useActionState hook shines.


What Is the useActionState Hook?

The useActionState hook in React 19 simplifies form submissions by managing form state updates and server actions together.

No need for useState or manual event handlers anymore.

It takes three parameters:

  • actionFn: A function called when the form is submitted. It takes two arguments:
  1. A previous state
  2. Form data (type FormData)
    • initialState: The initial value for the form state. This is used only before the first submission.
    • formRef (optional): A ref to the form element. Helps automatically collect form data when the form is submitted.

It returns an array with:

  • currentState: The latest form state returned by actionFn
  • action: A function to assign to the form's action attribute (<form action={action} ...>)
  • isPending: A boolean indicating whether the form is currently submitting (great for loading states)

Working Example: Hands-on With useActionState

Now that we've covered how useActionState works, let's put it into practice with a simple form component — the same one we've been using earlier.

Action Function:

export type FormState = {
  user: string;
  fruit: string;
  fruits: string[];
  select: string;
};

export const submitForm = (prevState: FormState, formData: FormData): FormState => {
  const user = formData.get("user")?.toString() || "";
  const fruit = formData.get("fruit")?.toString() || "";
  const fruits = formData.getAll("fruits").map(String);
  const select = formData.get("select")?.toString() || "";

  console.log({ user, fruit, fruits, select });

  return {
    ...prevState,
    user,
    fruit,
    fruits,
    select,
  };
};
Enter fullscreen mode Exit fullscreen mode

Updated Form Component:

import { useActionState } from "react";
import { submitForm } from "../actions/submitForm";
import type { FormState } from "../actions/submitForm";

export default function Form() {
  const [formState, formAction] = useActionState<FormState, FormData>(
    submitForm,
    {
      user: "",
      fruit: "",
      fruits: [],
      select: "",
    }
  );

  return (
    <div className="form-cont">
      <form action={formAction}>
        <input
          type="text"
          placeholder="user"
          name="user"
          defaultValue={formState.user}
        />
        <select name="select" defaultValue={formState.select}>
          <option disabled value="">
            --
          </option>
          <option>Banana</option>
          <option>Apple</option>
          <option>Orange</option>
        </select>

        <div>
          <label>
            <input
              type="radio"
              name="fruit"
              value="orange"
              defaultChecked={formState.fruit === "orange"}
            />
            Orange
          </label>
          <label>
            <input
              type="radio"
              name="fruit"
              value="apple"
              defaultChecked={formState.fruit === "apple"}
            />
            Apple
          </label>
          <label>
            <input
              type="radio"
              name="fruit"
              value="banana"
              defaultChecked={formState.fruit === "banana"}
            />
            Banana
          </label>
        </div>

        <div>
          <label>
            <input
              type="checkbox"
              name="fruits"
              value="orange"
              defaultChecked={formState.fruits.includes("orange")}
            />
            Orange
          </label>
          <label>
            <input
              type="checkbox"
              name="fruits"
              value="apple"
              defaultChecked={formState.fruits.includes("apple")}
            />
            Apple
          </label>
          <label>
            <input
              type="checkbox"
              name="fruits"
              value="banana"
              defaultChecked={formState.fruits.includes("banana")}
            />
            Banana
          </label>
        </div>

        <button>Submit</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we call the useActionState hook with our submitForm action and provide an initial form state. One little but important detail — we add defaultValue and defaultChecked to the inputs. Why? Because when you submit the form, the inputs reset. This helps retain the values after submission. If needed, you could also implement a resetValues function.


A Fully Working Demo

👉 Check out this CodeSandbox


About the Author

Bharat has been a Front-End developer since 2011. He has a thing for building great developer experience. He loves learning and teaching all things tech.

Find him on Twitter, GitHub, and LinkedIn.

Top comments (0)