Advanced React Forms: FormData, Validation Rules and React Hook Form

React.js Full Course: Beginner to Advanced · Hooks and Data · 10 min read · Updated

A form with FormData

  1. The browser keeps the values of a form. Every input needs a name.
  2. On submit, new FormData reads all fields at once. No state for each field is needed.
  3. A plain validation function checks the data and returns an object of errors.
  4. If there are errors, show a clear message next to each field. If not, send the data.
  5. The server must check everything again. Browser checks can be skipped.

In lesson 9 you built forms with controlled inputs, where every key press goes through state. That is great to learn the idea, but it is not the only way, and for big forms it can be too much code. In this lesson you will learn the shorter way with FormData, how to write clean validation, and how a form library can help.

The browser already knows your form

A normal HTML form already keeps the values of its fields. When the person submits, the browser can hand you all values at once with FormData. You do not need state for every field.

function ContactForm() {
  function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const data = Object.fromEntries(formData);
    console.log(data);   // { name: '...', email: '...', message: '...' }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>Name <input name="name" required /></label>
      <label>Email <input name="email" type="email" required /></label>
      <label>Message <textarea name="message" /></label>
      <button type="submit">Send</button>
    </form>
  );
}
  • Every input needs a `name`. That name becomes the key in the data.
  • event.currentTarget is the form itself.
  • Object.fromEntries(formData) turns the form data into a plain object.
  • There is no state at all, and no redraw while the person types.

Checkboxes with the same name give several values. Use formData.getAll('hobby') to read them as an array. A file input gives a File object, so you can send the whole formData to a server with fetch(url, { method: 'POST', body: formData }). Do not set the Content-Type header yourself in that case, because the browser must add the right boundary.

Use the browser's own validation first

HTML gives you free checks: required, type="email", minLength, maxLength, min, max and pattern. The browser shows a message and stops the submit. This is a good first layer, and it works without any JavaScript.

<input name="age" type="number" min="18" max="99" required />
<input name="code" pattern="[0-9]{6}" title="Six digits" required />

The built-in bubbles look different in each browser, and you cannot style them easily. When you need your own messages, add noValidate to the form and write your own checks.

Writing your own validation function

Put the rules in one plain function that takes the data and returns an object of errors. It is easy to read and easy to test, and it does not depend on React.

export function validateSignUp(data) {
  const errors = {};

  if (!data.name || data.name.trim().length < 2) {
    errors.name = 'Please enter your name (at least 2 letters).';
  }
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email || '')) {
    errors.email = 'Please enter a valid email address.';
  }
  if (!data.password || data.password.length < 8) {
    errors.password = 'Your password needs at least 8 characters.';
  }
  return errors;
}
import { useState } from 'react';
import { validateSignUp } from './validate';

function SignUpForm() {
  const [errors, setErrors] = useState({});
  const [done, setDone] = useState(false);

  function handleSubmit(event) {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(event.currentTarget));
    const found = validateSignUp(data);
    setErrors(found);
    if (Object.keys(found).length > 0) return;
    setDone(true);   // here you would send data to the server
  }

  if (done) return <p role="status">Thank you! Your account is ready.</p>;

  return (
    <form onSubmit={handleSubmit} noValidate>
      <Field label="Name" name="name" error={errors.name} />
      <Field label="Email" name="email" type="email" error={errors.email} />
      <Field label="Password" name="password" type="password" error={errors.password} />
      <button type="submit">Create account</button>
    </form>
  );
}

function Field({ label, name, type = 'text', error }) {
  const errorId = name + '-error';
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input id={name} name={name} type={type}
        aria-invalid={error ? 'true' : 'false'}
        aria-describedby={error ? errorId : undefined} />
      {error && <p id={errorId} role="alert">{error}</p>}
    </div>
  );
}

The Field component keeps the label, the input and the error together, so every field is accessible without extra work. You wrote the rules once in validateSignUp, and you can test it with plain calls.

When to show errors

  • On submit: the simplest. Show all errors together, and move focus to the first wrong field.
  • On blur (when the person leaves a field): friendlier for long forms. Do not show an error while the person is still typing their first attempt.
  • Clear the error as soon as the value becomes valid, so people see they fixed it.

Write messages that tell people what to do: “Please enter a valid email address” is better than “Invalid input”. Do not blame the person. And never rely only on colour, since some people cannot tell red from green: always include text.

Server validation always

const response = await fetch('/api/signup', { method: 'POST', body: formData });
if (response.status === 409) {
  setErrors({ email: 'This email is already registered. Try signing in.' });
  return;
}

React Hook Form: when a library helps

For large forms with many rules, a library saves time. React Hook Form is popular because it uses uncontrolled inputs, so the form does not redraw on every key press. You register fields and get errors from it:

npm install react-hook-form
import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();

  async function onSubmit(data) {
    await fakeLogin(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" type="email"
        {...register('email', { required: 'Email is required' })} />
      {errors.email && <p role="alert">{errors.email.message}</p>}

      <label htmlFor="password">Password</label>
      <input id="password" type="password"
        {...register('password', { required: 'Password is required', minLength: { value: 8, message: 'At least 8 characters' } })} />
      {errors.password && <p role="alert">{errors.password.message}</p>}

      <button disabled={isSubmitting}>Log in</button>
    </form>
  );
}

register('email', rules) connects an input to the form and gives it its rules. handleSubmit runs your function only when all rules pass. isSubmitting is true while your async function runs, which is handy for disabling the button. For complex rules, you can combine it with a schema library such as Zod. Start with the plain way in this lesson, and add a library when your forms become big.

Good form habits

  • Every input has a visible label. A placeholder disappears when typing, so it is not a label.
  • Use the right type and autoComplete values (email, tel, current-password, one-time-code). Phones then show the right keyboard and can fill in details.
  • Disable the submit button while sending, and show progress.
  • Do not clear a long form when an error happens. Keep what people typed.
  • Keep forms short. Every extra field loses some people.

Common mistakes

  • Forgetting name on inputs, so FormData is empty.
  • Setting the Content-Type header when sending FormData.
  • Checking only in the browser and not on the server.
  • Showing errors only in red with no text.
  • Using value without onChange, or mixing controlled and uncontrolled inputs in the same field.

Choosing your approach: a short guide

You now know three ways to build a form. Here is how to pick between them for a real project.

  • Controlled inputs with state: best when the screen must react while people type, for example a live character counter, instant formatting or a search that filters as you type.
  • `FormData` with a validation function: best for most normal forms, such as contact, sign-up and settings. Short code, no state for each field, and an easy function to test.
  • React Hook Form: best for large forms with many rules, many steps or dynamic fields, where you want less code and fewer redraws.

Whatever you choose, the same good habits apply: clear labels, helpful messages, the right input types, a disabled button while sending, and a second check on the server. A form that is easy to use is worth more than a clever one.

Questions beginners often ask

Can I use FormData with a controlled input? You can, but it mixes two ways. Pick one way for each form so the code stays easy to understand.

How do I validate on blur? Add onBlur to the field and run the checks for that one field, then set only that field error. Clear it when the value becomes valid.

How do I reset a form after submit? For an uncontrolled form, call event.currentTarget.reset(). For a controlled form, set the state back to the starting object. In React 19 a form action resets the form for you after it finishes.

What is Zod? A small library for describing the shape of data and checking it, with clear errors. You can use the same rules in the browser and on a Node server.

Your task

  1. Build the sign-up form with FormData, noValidate and the validateSignUp function. Add a confirm password field to the rules.
  2. Move focus to the first field with an error after a failed submit (use a ref or querySelector).
  3. Show an error from a fake server response, such as “email already used”.
  4. Rebuild the login form with React Hook Form and compare the code.

How to check it worked

  • Every error appears next to its field, and disappears when fixed.
  • A keyboard user can reach and fix every field.
  • You can read the form values without any useState for fields.

Summary

  • FormData reads all fields of a form on submit. Give every input a name.
  • Use HTML validation as a first layer, and write one plain validation function for your own messages.
  • Show clear, helpful text errors next to fields and connect them with aria-describedby.
  • Always validate again on the server. Use React Hook Form when forms get large.

Frequently asked questions

Should I use controlled or uncontrolled inputs?

Use controlled inputs when the screen must react as people type. Use FormData and uncontrolled inputs for simple or very large forms where you only need the values on submit.

Do I need React Hook Form?

Not for small forms. It helps with large forms and many rules, and it avoids a redraw on every key press.

Is client side validation enough?

No. Anyone can send a request without your form. The server must always validate the data again.

Why is FormData empty?

Inputs without a name attribute are not included. Give every input a name.