> ## Content Index
> Fetch the complete content index at: https://reactpractice.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Building a simple form in React - before and after React 19
- URL: https://reactpractice.dev/articles/building-a-simple-form-in-react-before-and-after-react-19/
- Published: 2024-12-18T07:04:26.000Z
- Updated: 2026-06-01T01:59:08.000Z
- Author: Corina Udrescu
- Tags: Articles, #corina-2, #Import 2026-06-09 12:07

React 19 was just launched with a lot of new improvements to how we use forms.

I wanted to get a before and after look of how to build forms in React, so I created a simple newsletter subscribe box to showcase the differences:  

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/1-subscribe-box.png)

Here is how the form would look like before React 19:

- the form is submitted `onSubmit`
- the form inputs are controlled and their values are saved in state
- loading and error states are manually handled

```
import { useState } from "react";
import "./newsletter.css";

const fakeSendEmail = async () => {
  return new Promise((resolve) => setTimeout(resolve, 1000));
};

const NewsletterSubscribe = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [result, setResult] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();

    if (!name || !email) {
      setResult({
        type: "error",
        message: `Please fill in your name and email.`,
      });
      return;
    }

    setIsPending(true);
    fakeSendEmail().then(() => {
      setResult({
        type: "success",
        message: `You have succesfully subscribed!`,
      });
      setName("");
      setEmail("");
      setIsPending(false);
    });
  };
  return (
    <>
      {result && <p className={`message ${result.type}`}>{result.message}</p>}
      {isPending && <p className="message loading">Loading ...</p>}
      <form onSubmit={handleSubmit}>
        <h3>Join the newsletter</h3>
        <div>
          <label htmlFor="name">Name</label>
          <input
            type="text"
            id="name"
            name="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            name="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <button type="submit">Subscribe</button>
        </div>
      </form>
    </>
  );
};

export default NewsletterSubscribe;

```

Now here is the same form with React 19

- form uses `useActionState` hook to delegate handling of the loading and error states
- form is submitted with the `action` attribute instead of `onSubmit`
- form no longer needs controlled inputs, as the values are correctly populated (and cleared!) when we use the `action` attribute to submit

```
import { useActionState } from "react";
import "./newsletter.css";

const fakeSendEmail = async () => {
  return new Promise((resolve) => setTimeout(resolve, 1000));
};

const NewsletterSubscribe = () => {
  const [result, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const email = formData.get("email");
      const name = formData.get("name");

      if (!name || !email) {
        return {
          type: "error",
          message: `Please fill in your name and email.`,
        };
      }

      await fakeSendEmail();

      return {
        type: "success",
        message: `You have succesfully subscribed!`,
      };
    },
    null
  );

  return (
    <>
      {result && <p className={`message ${result.type}`}>{result.message}</p>}
      {isPending && <p className="message loading">Loading ...</p>}
      <form action={submitAction}>
        <h3>Join the newsletter</h3>
        <div>
          <label htmlFor="name">Name</label>
          <input type="text" id="name" name="name" />
        </div>
        <div>
          <label htmlFor="email">Email</label>
          <input type="email" id="email" name="email" />
        </div>
        <div>
          <button type="submit">Subscribe</button>
        </div>
      </form>
    </>
  );
};

export default NewsletterSubscribe;

```

Let's go over the differences step by step.

💡

Note that the intermediate steps have broken code, but I kept it to make the diffs easier to grasp.

### Submit forms with the `action` attribute instead of `onSubmit`

React DOM now supports `<form>` actions - which means you can pass functions to a form's `action` property and React will take care of submitting the form for you.

This is great because it means that for example, you no longer need to manually call `e.preventDefault()`.

The `useActionState` hook returns the exact function you can pass to the form `action`\- which is just a wrapped submit handler.

You can read more about the new `action` attribute in the official release notes: [https://react.dev/blog/2024/12/05/react-19#form-actions](https://react.dev/blog/2024/12/05/react-19?ref=reactpractice.dev#form-actions).

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/2-use-action-state.png)

### Let React handle error and success states for you

Before, we would be manually handling the success and error states after submitting a form - usually by saving them as state variables.

With the new `useActionState` form, you can just return the values you need, and they will be available as the first argument of the `useActionState` hook.

In the example below, you can see the `result` variable is now populated with what is returned from the submit handler:

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/3-result.png)

Most examples I've seen of this feature just return the `error` \- and that's also fine!  
Check out the official release notes for more examples: [https://react.dev/blog/2024/12/05/react-19#new-hook-useactionstate](https://react.dev/blog/2024/12/05/react-19?ref=reactpractice.dev#new-hook-useactionstate)

### Let React handle the loading state for you

The `useActionState` hook returns the loading state of the form as the third argument, so you no longer need to track this manually!

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/4-loading.png)

### Let react handle the input values state for you

Say goodbye to controlled form inputs! When using the form `action` property to submit, the form values are tracked for you! In the submit handler, you can read them from the `formData` parameter - which uses the native [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData?ref=reactpractice.dev) HTML API.

Notice how you no longer need to clear values after submit either - React does that for you (but it can be manually triggered as well if you need to with [requestFormReset](https://react.dev/blog/2024/12/05/react-19?ref=reactpractice.dev#form-actions))

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/5-form-inputs.png)

### Have async submit handlers

Before, because we would use a regular event handler to submit the form, we couldn't have async functions as the submit handler. This is no longer the case, so you can use `async/await` as needed!

![](https://storage.ghost.io/c/7c/fc/7cfc444f-a1c9-4dd3-a87e-8dcc2f1180be/content/images/2024/12/6-async-submit-handler.png)

### That's a wrap

Checkout the full working code over on Github: [https://github.com/reactpractice-dev/newsletter-subscribe-react19](https://github.com/reactpractice-dev/newsletter-subscribe-react19?ref=reactpractice.dev)

What do you think of the new React form features? Let me know in the comments below!