3 min read

3 ways to build forms in react (without any libraries)

Using a library when building complex forms is likely still very helpful - react-hook-form with zod is a robust solution used in production in many applications today. But if you just want to build a quick form, do you really need a library?

I used to jump to building a quick form with controlled inputs in the past, but lately I realised going for a default browser form with uncontrolled inputs works even nicer!

Here is an overview of three approaches for building forms in React without any 3rd party libraries by building a "user feedback" form.

<FeedbackForm 
	feedback={itemBeingEdited || undefined}
	onSave={handleSave}
	onCancel={() => setItemBeingEdited(null)}
/>

1 - Form with uncontrolled inputs

This is my current preferred way, as it just uses native browser features and the code is very readable:

  • form fields need to have the name attribute specified so we can get the values with FormData (docs)
  • we can clear the values with form.reset() (docs)
import { FormEvent, MouseEvent } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";

type Props = {
  // optional prop - initial value in case we are editing an item
  feedback?: UserFeedback;
  onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
  onCancel: () => void;
};

const UncontrolledForm: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const form = e.target as HTMLFormElement;
    const data = new FormData(form);
    onSave({
      ...feedback,
      name: data.get("name") as string,
      suggestion: data.get("suggestion") as string,
    });
    form.reset();
  };

  const handleReset = (e: MouseEvent<HTMLButtonElement>) => {
    const form = (e.target as HTMLButtonElement).form as HTMLFormElement;
    form.reset();

    onCancel();
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" name="name" defaultValue={feedback?.name} />
      </label>
      <label>
        Suggestion:
        <input
          type="text"
          name="suggestion"
          defaultValue={feedback?.suggestion}
        />
      </label>
      <div>
        <button>Save</button>
        <button type="button" onClick={handleReset}>
          Cancel
        </button>
      </div>
    </form>
  );
};

export default UncontrolledForm;

2 - Form with controlled inputs

This is the approach you will probably find most often in tutorials out there.
It used to be the go to for forms and it's still useful if you need the values in places other than the submit handler itself.

import { FormEvent, useState } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";

type Props = {
  // optional prop - initial value in case we are editing an item
  feedback?: UserFeedback;
  onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
  onCancel: () => void;
};

const ControlledForm: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
  const [name, setName] = useState<string>(feedback?.name || "");
  const [suggestion, setSuggestion] = useState<string>(
    feedback?.suggestion || ""
  );
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    onSave({
      ...feedback,
      name,
      suggestion,
    });
    setName("");
    setSuggestion("");
  };

  const handleReset = () => {
    setName("");
    setSuggestion("");
    onCancel();
  };
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <label>
        Suggestion:
        <input
          type="text"
          value={suggestion}
          onChange={(e) => setSuggestion(e.target.value)}
        />
      </label>
      <div>
        <button>Save</button>
        <button type="button" onClick={handleReset}>
          Cancel
        </button>
      </div>
    </form>
  );
};

export default ControlledForm;

3 - Form with React 19 action attribute

This is the new approach introduced with React 19 - using the action attribute on the form element, together with the useActionState hook. For simple forms, I find this to be too verbose, but it shines when doing async server actions, or even better, when used with SSR.

It's nice that the fields get reset for you on submit (though sometimes you might not want that, like when there's an error 😅).

import { useActionState, MouseEvent } from "react";
import { UserFeedback, UserFeedbackFormFields } from "../types";

type Props = {
  // optional prop - initial value in case we are editing an item
  feedback?: UserFeedback;
  onSave: (newValues: UserFeedbackFormFields | UserFeedback) => void;
  onCancel: () => void;
};

const React19Form: React.FC<Props> = ({ feedback, onSave, onCancel }) => {
  const [_, submitAction] = useActionState(
    async (_: UserFeedbackFormFields, formData: FormData) => {
      onSave({
        ...feedback,
        name: formData.get("name") as string,
        suggestion: formData.get("suggestion") as string,
      });

      return "form saved successfully";
    },
    null
  );

  const handleReset = (e: MouseEvent<HTMLButtonElement>) => {
    const form = (e.target as HTMLButtonElement).form as HTMLFormElement;
    form.reset();

    onCancel();
  };

  return (
    <form action={submitAction}>
      <label>
        Name:
        <input type="text" name="name" defaultValue={feedback?.name} />
      </label>
      <label>
        Suggestion:
        <input
          type="text"
          name="suggestion"
          defaultValue={feedback?.suggestion}
        />
      </label>
      <div>
        <button>Save</button>
        <button type="button" onClick={handleReset}>
          Cancel
        </button>
      </div>
    </form>
  );
};

export default React19Form;

You can checkout the full working code for the three forms above on CodeSandbox: https://codesandbox.io/p/sandbox/three-ways-to-build-forms-2s9qpd.

How do you build simple forms with React?
Do you prefer any version over the other? Why?

Get the React Practice Calendar!

28 days of focused practice of increasing difficulty, going through everything from Fundamentals, Data fetching, Forms and using Intervals in React.

You will also get notified whenever a new challenge is published.