React Forms

Forms are an essential part of almost every web application.

Users use forms to:

  • Create accounts
  • Log in
  • Search for content
  • Submit feedback
  • Update profiles
  • Place orders
  • Upload information
  • Contact businesses
  • Filter products
  • Enter payment details

In traditional HTML, forms are usually handled directly by the browser. React gives you more control by allowing form values, validation, submission, and UI feedback to be connected to component state.

React forms are especially important because they introduce several concepts that appear repeatedly in real-world applications:

  • State management
  • Event handling
  • Controlled components
  • Uncontrolled components
  • Validation
  • Error handling
  • Form submission
  • Dynamic fields

By the end of this chapter, you will understand how to build forms ranging from a simple input field to a dynamic form with multiple fields and validation.

React Forms Introduction

A form in React is still built using normal HTML elements such as:

<form>
  <input />
  <textarea />
  <select>
    <option>Option 1</option>
  </select>
  <button type="submit">Submit</button>
</form>

The difference is that React can manage the values and behavior of these elements.

For example:

import { useState } from "react";

function LoginForm() {
  const [email, setEmail] = useState("");

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
      />
    </form>
  );
}

Here, React state stores the current value of the input.

When the user types:

hello@example.com

the state is updated to contain the same value.

This creates a connection between the form element and React state.

Form Elements

React forms use standard HTML form elements.

The most common elements are:

ElementCommon Purpose
<input>Text, email, password, checkbox, radio, number, etc.
<textarea>Multi-line text
<select>Selecting an option
<option>An option inside <select>
<button>Submit or perform an action
<form>Groups form controls and handles submission

For example:

<form>
  <input type="text" />
  
  <textarea />
  
  <select>
    <option value="react">React</option>
    <option value="javascript">JavaScript</option>
  </select>

  <button type="submit">
    Submit
  </button>
</form>

React does not replace HTML forms. It gives you JavaScript and state-based control over them.

Input Fields

The <input> element is one of the most frequently used form elements.

A basic input looks like this:

<input type="text" />

You can specify different input types:

<input type="text" />
<input type="email" />
<input type="password" />
<input type="number" />
<input type="date" />
<input type="search" />

Each type provides behavior appropriate to the kind of information being collected.

Handling an Input

You can listen for changes using onChange:

function App() {
  function handleChange(event) {
    console.log(event.target.value);
  }

  return (
    <input
      type="text"
      onChange={handleChange}
    />
  );
}

event.target.value contains the current value of the input.

Connecting an Input to State

A common React pattern is:

import { useState } from "react";

function App() {
  const [name, setName] = useState("");

  return (
    <input
      type="text"
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  );
}

Now the state and input are connected.

The value comes from:

value={name}

and changes are sent back through:

onChange={(event) => setName(event.target.value)}

This is the foundation of a controlled input.

Placeholder

You can provide a hint using placeholder:

<input
  type="email"
  placeholder="Enter your email"
/>

A placeholder should provide an example or hint rather than act as the only label for an important field.

Labels

Use labels to make forms easier to understand and more accessible.

<label htmlFor="email">
  Email Address
</label>

<input
  id="email"
  type="email"
/>

The htmlFor value matches the input’s id.

Textarea

The <textarea> element is used for multi-line text.

For example:

<textarea
  placeholder="Write your message"
/>

In React, a controlled textarea can be created like this:

import { useState } from "react";

function FeedbackForm() {
  const [message, setMessage] = useState("");

  return (
    <textarea
      value={message}
      onChange={(event) => setMessage(event.target.value)}
      placeholder="Write your feedback"
    />
  );
}

The value is stored in React state.

Textarea and HTML

In HTML, you may see:

<textarea>
Default text
</textarea>

In React, when controlling the value, use the value prop:

<textarea value={message} />

For an initial uncontrolled value, React provides defaultValue:

<textarea defaultValue="Initial message" />

This distinction becomes important when learning controlled and uncontrolled components.

Select

The <select> element lets users choose from a list of options.

For example:

<select>
  <option value="react">React</option>
  <option value="vue">Vue</option>
  <option value="angular">Angular</option>
</select>

Controlled Select

You can connect a select element to state:

import { useState } from "react";

function CourseForm() {
  const [course, setCourse] = useState("react");

  return (
    <select
      value={course}
      onChange={(event) => setCourse(event.target.value)}
    >
      <option value="react">React</option>
      <option value="javascript">JavaScript</option>
      <option value="python">Python</option>
    </select>
  );
}

The selected option is stored in course.

Multiple Select

A select element can also allow multiple choices.

<select multiple>
  <option value="react">React</option>
  <option value="javascript">JavaScript</option>
  <option value="python">Python</option>
</select>

When controlling a multiple select, the value should be an array.

<select
  multiple
  value={skills}
  onChange={handleChange}
>

You then need to collect the selected options from the event.

Checkbox

Checkboxes are useful when users can independently enable or disable an option.

For example:

<input type="checkbox" />
<label>Subscribe to newsletter</label>

Checkboxes use the checked property rather than value to represent whether they are selected.

A controlled checkbox looks like this:

import { useState } from "react";

function NewsletterForm() {
  const [subscribed, setSubscribed] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={subscribed}
        onChange={(event) =>
          setSubscribed(event.target.checked)
        }
      />
      Subscribe to newsletter
    </label>
  );
}

The important property is:

event.target.checked

It returns either:

true

or:

false

Checkbox with a Label

A more accessible pattern is:

<label htmlFor="terms">
  <input
    id="terms"
    type="checkbox"
  />
  I agree to the terms
</label>

The label makes it easier for users to understand what the checkbox represents.

Radio Buttons

Radio buttons are useful when users should select one option from a group.

For example:

<label>
  <input
    type="radio"
    name="plan"
    value="free"
  />
  Free
</label>

<label>
  <input
    type="radio"
    name="plan"
    value="pro"
  />
  Pro
</label>

The same name groups the radio buttons together.

Controlled Radio Buttons

import { useState } from "react";

function PlanForm() {
  const [plan, setPlan] = useState("free");

  return (
    <>
      <label>
        <input
          type="radio"
          name="plan"
          value="free"
          checked={plan === "free"}
          onChange={(event) => setPlan(event.target.value)}
        />
        Free
      </label>

      <label>
        <input
          type="radio"
          name="plan"
          value="pro"
          checked={plan === "pro"}
          onChange={(event) => setPlan(event.target.value)}
        />
        Pro
      </label>
    </>
  );
}

Here, only one option can be selected.

The state contains:

plan

and might contain either:

"free"

or:

"pro"

Multiple Inputs

Real forms usually contain several inputs.

For example:

function RegistrationForm() {
  return (
    <form>
      <input type="text" />
      <input type="email" />
      <input type="password" />
      <button type="submit">
        Create Account
      </button>
    </form>
  );
}

You could create separate state variables:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

This is perfectly valid.

However, when several fields belong to the same form, an object can sometimes provide a convenient structure.

const [formData, setFormData] = useState({
  name: "",
  email: "",
  password: ""
});

Handling Multiple Inputs with One Handler

You can give each input a matching name:

<input
  name="name"
  value={formData.name}
  onChange={handleChange}
/>

<input
  name="email"
  value={formData.email}
  onChange={handleChange}
/>

<input
  name="password"
  value={formData.password}
  onChange={handleChange}
/>

Then:

function handleChange(event) {
  const { name, value } = event.target;

  setFormData((currentData) => ({
    ...currentData,
    [name]: value
  }));
}

The computed property:

[name]: value

updates the correct field.

For example, if:

name === "email"

React effectively updates:

email: value

without replacing the other fields.

Image Placement: Multiple Form Inputs

Place this image immediately after the multiple inputs section.

Create a modern 16:9 educational infographic showing a React registration form with Name, Email, Password, Country, and checkbox fields. Visually connect the fields to a central React state object containing the same field names. Show a simple flow from user input to onChange to state. Use a modern Web4Learners developer aesthetic, clean typography, rounded cards, subtle gradients, polished UI, and minimal text.

Form Submission

HTML forms normally submit when the user presses a submit button.

In React, you can handle submission with onSubmit.

function LoginForm() {
  function handleSubmit(event) {
    event.preventDefault();

    console.log("Form submitted");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" />
      <input type="password" />

      <button type="submit">
        Login
      </button>
    </form>
  );
}

Why Use preventDefault()?

By default, the browser may submit the form using its normal HTML behavior, which can cause a page navigation or reload.

React applications commonly prevent that default behavior so JavaScript can handle the submission.

event.preventDefault();

Then you can:

  • Validate the form
  • Send data to an API
  • Show a loading state
  • Display errors
  • Show a success message

Complete Submission Example

import { useState } from "react";

function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function handleSubmit(event) {
    event.preventDefault();

    console.log({
      email,
      password
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">
        Email
      </label>

      <input
        id="email"
        type="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
      />

      <label htmlFor="password">
        Password
      </label>

      <input
        id="password"
        type="password"
        value={password}
        onChange={(event) =>
          setPassword(event.target.value)
        }
      />

      <button type="submit">
        Login
      </button>
    </form>
  );
}

Controlled Components

A controlled component is a form element whose current value is controlled by React state.

For example:

const [email, setEmail] = useState("");

and:

<input
  value={email}
  onChange={(event) => setEmail(event.target.value)}
/>

React is the source of truth for the input value.

The flow looks like this:

User types
    ↓
onChange
    ↓
setState
    ↓
React re-renders
    ↓
Input receives updated value

Controlled Input Example

function SearchForm() {
  const [search, setSearch] = useState("");

  return (
    <input
      value={search}
      onChange={(event) => setSearch(event.target.value)}
      placeholder="Search tutorials"
    />
  );
}

Now React always knows what the user has entered.

Why Use Controlled Components?

Controlled components are useful when you need to:

  • Validate input while typing
  • Enable or disable buttons
  • Display character counts
  • Show conditional UI
  • Transform input
  • Share form data with other components
  • Submit data to APIs
  • Track form state

For example:

const isValid = email.includes("@");

You can use this information immediately because React knows the current value.

Controlled Checkbox

Remember that checkboxes use checked:

<input
  type="checkbox"
  checked={accepted}
  onChange={(event) =>
    setAccepted(event.target.checked)
  }
/>

Controlled Select

<select
  value={category}
  onChange={(event) => setCategory(event.target.value)}
>

The same principle applies.

Uncontrolled Components

An uncontrolled component allows the DOM to maintain the current form value instead of storing every value in React state.

You can use a ref to access the value when needed.

import { useRef } from "react";

function LoginForm() {
  const emailRef = useRef(null);

  function handleSubmit(event) {
    event.preventDefault();

    console.log(emailRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        ref={emailRef}
      />

      <button type="submit">
        Submit
      </button>
    </form>
  );
}

Here, React does not update state on every keystroke.

Instead, the DOM keeps track of the current value.

defaultValue

For an uncontrolled input, you can provide an initial value with defaultValue:

<input
  type="text"
  defaultValue="John"
/>

Unlike value, defaultValue sets the initial value without making React control subsequent changes.

Controlled vs Uncontrolled

ControlledUncontrolled
React state stores the valueDOM stores the value
Uses value or checkedOften uses defaultValue or defaultChecked
Uses onChange to update stateCan use refs to read the value
Easy to validate while typingUseful when direct DOM form access is convenient
More explicit state managementLess state code for simple cases

For many React applications, controlled components are a straightforward default when form state needs to participate in the UI.

Uncontrolled inputs can also be useful, especially for simple forms or when integrating with APIs and libraries that work directly with the DOM.

Form Validation

Form validation checks whether user-provided information meets your application’s requirements.

For example:

  • Email must be provided
  • Password must be long enough
  • Name cannot be empty
  • Terms must be accepted
  • Age must be within a valid range

HTML already provides basic browser validation.

For example:

<input
  type="email"
  required
/>

and:

<input
  type="password"
  required
  minLength={8}
/>

However, React applications often need additional validation logic.

Basic React Validation

function validateForm(formData) {
  const errors = {};

  if (!formData.name.trim()) {
    errors.name = "Name is required";
  }

  if (!formData.email.includes("@")) {
    errors.email = "Enter a valid email address";
  }

  if (formData.password.length < 8) {
    errors.password =
      "Password must be at least 8 characters";
  }

  return errors;
}

You can call this function during submission.

function handleSubmit(event) {
  event.preventDefault();

  const validationErrors = validateForm(formData);

  if (Object.keys(validationErrors).length > 0) {
    setErrors(validationErrors);
    return;
  }

  // Submit valid data
}

Client-Side Validation Is Not Enough

Client-side validation improves user experience, but it should not be treated as the only security boundary.

If your application sends form data to a server, the server should validate the submitted data again.

The browser and React code are controlled by the client and cannot be trusted as the final authority.

Error Messages

Validation is most useful when users can clearly understand what needs to be corrected.

For example:

{errors.email && (
  <p>{errors.email}</p>
)}

If the error exists, it is displayed.

Complete Error Example

function LoginForm() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  function handleSubmit(event) {
    event.preventDefault();

    if (!email.trim()) {
      setError("Email is required");
      return;
    }

    setError("");

    console.log("Form submitted");
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">
        Email
      </label>

      <input
        id="email"
        type="email"
        value={email}
        onChange={(event) => {
          setEmail(event.target.value);
          setError("");
        }}
        aria-invalid={Boolean(error)}
        aria-describedby={
          error ? "email-error" : undefined
        }
      />

      {error && (
        <p id="email-error">
          {error}
        </p>
      )}

      <button type="submit">
        Submit
      </button>
    </form>
  );
}

The aria-invalid and aria-describedby attributes help assistive technologies understand the relationship between the input and its error message.

Multiple Errors

For larger forms, an object is useful:

const [errors, setErrors] = useState({});

For example:

{
  name: "Name is required",
  email: "Enter a valid email",
  password: "Password is too short"
}

Then each field can display its own message.

Form Reset

After submitting a form, you may want to clear its fields.

Suppose:

const [formData, setFormData] = useState({
  name: "",
  email: "",
  message: ""
});

You can reset it by restoring the initial object:

const initialForm = {
  name: "",
  email: "",
  message: ""
};

setFormData(initialForm);

Reset Button

HTML provides a reset button:

<button type="reset">
  Reset
</button>

However, when using controlled components, simply using the browser’s reset behavior does not update React state in the same way as changing the state itself.

For controlled forms, it is often clearer to reset the state explicitly.

function handleReset() {
  setFormData(initialForm);
  setErrors({});
}

Then:

<button
  type="button"
  onClick={handleReset}
>
  Reset
</button>

Resetting After Submission

function handleSubmit(event) {
  event.preventDefault();

  // Validate and submit...

  setFormData(initialForm);
  setErrors({});
}

Whether you reset immediately depends on the application’s behavior.

For example, a contact form might clear after a successful submission, while an edit form might keep the submitted values visible.

Dynamic Forms

A dynamic form allows users to add, remove, or modify fields while the application is running.

Examples include:

  • Adding multiple phone numbers
  • Adding education records
  • Adding work experience
  • Adding product variants
  • Adding multiple addresses
  • Adding multiple skills

Instead of creating a fixed number of fields, you store the fields in an array.

Basic Dynamic Form

import { useState } from "react";

function SkillsForm() {
  const [skills, setSkills] = useState([
    { id: 1, value: "" }
  ]);

  return (
    <div>
      {skills.map((skill) => (
        <input
          key={skill.id}
          value={skill.value}
          onChange={(event) => {
            setSkills((currentSkills) =>
              currentSkills.map((item) =>
                item.id === skill.id
                  ? {
                      ...item,
                      value: event.target.value
                    }
                  : item
              )
            );
          }}
        />
      ))}
    </div>
  );
}

The important idea is that each field has an ID.

Adding a Field

function addSkill() {
  setSkills((currentSkills) => [
    ...currentSkills,
    {
      id: crypto.randomUUID(),
      value: ""
    }
  ]);
}

Then:

<button type="button" onClick={addSkill}>
  Add Skill
</button>

Each click adds another field.

Removing a Field

function removeSkill(id) {
  setSkills((currentSkills) =>
    currentSkills.filter((skill) => skill.id !== id)
  );
}

Then:

<button
  type="button"
  onClick={() => removeSkill(skill.id)}
>
  Remove
</button>

This creates a complete add/remove pattern.

Complete Dynamic Form

import { useState } from "react";

function SkillsForm() {
  const [skills, setSkills] = useState([
    {
      id: crypto.randomUUID(),
      value: ""
    }
  ]);

  function addSkill() {
    setSkills((currentSkills) => [
      ...currentSkills,
      {
        id: crypto.randomUUID(),
        value: ""
      }
    ]);
  }

  function updateSkill(id, value) {
    setSkills((currentSkills) =>
      currentSkills.map((skill) =>
        skill.id === id
          ? { ...skill, value }
          : skill
      )
    );
  }

  function removeSkill(id) {
    setSkills((currentSkills) =>
      currentSkills.filter(
        (skill) => skill.id !== id
      )
    );
  }

  function handleSubmit(event) {
    event.preventDefault();

    console.log(skills);
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Skills</h2>

      {skills.map((skill) => (
        <div key={skill.id}>
          <input
            value={skill.value}
            onChange={(event) =>
              updateSkill(
                skill.id,
                event.target.value
              )
            }
            placeholder="Enter a skill"
          />

          <button
            type="button"
            onClick={() => removeSkill(skill.id)}
          >
            Remove
          </button>
        </div>
      ))}

      <button
        type="button"
        onClick={addSkill}
      >
        Add Skill
      </button>

      <button type="submit">
        Submit
      </button>
    </form>
  );
}

This example demonstrates several important React concepts together:

  • Array state
  • Controlled inputs
  • map()
  • Stable keys
  • Immutable updates
  • Adding items
  • Removing items
  • Updating individual items
  • Form submission

Dynamic Forms and Keys

Dynamic forms are another situation where keys are extremely important.

Consider:

{skills.map((skill) => (
  <input
    key={skill.id}
    value={skill.value}
  />
))}

The key should identify the specific field.

If fields can be added, removed, or reordered, using the array index as the key can cause confusing behavior, particularly when the individual field contains local state or other identity-sensitive information.

Prefer a stable ID associated with the item:

key={skill.id}

If you create IDs yourself, make sure they remain stable for the lifetime of the item.

HTML Validation and React Validation

React forms can use both browser-level HTML validation and custom React validation.

For example:

<input
  type="email"
  required
/>

The browser can detect that the field is required and should contain a valid email format.

You can also add custom rules:

if (!email.endsWith("@example.com")) {
  errors.email =
    "Please use your company email address";
}

The right approach depends on your application.

HTML validation is useful for standard constraints.

React validation is useful for application-specific rules and custom error UI.

Server-side validation remains necessary whenever data is sent to a backend.

Loading State During Form Submission

Real applications often send form data to an API.

You may need a loading state:

const [isSubmitting, setIsSubmitting] = useState(false);

Then:

async function handleSubmit(event) {
  event.preventDefault();

  setIsSubmitting(true);

  try {
    // Send form data to API
  } finally {
    setIsSubmitting(false);
  }
}

The button can then respond to the submission state:

<button
  type="submit"
  disabled={isSubmitting}
>
  {isSubmitting ? "Submitting..." : "Submit"}
</button>

This prevents users from repeatedly submitting the same form while the request is being processed.

Complete React Form Example

Let’s combine the concepts into a practical registration form.

import { useState } from "react";

const initialForm = {
  name: "",
  email: "",
  password: "",
  role: "",
  newsletter: false
};

function RegistrationForm() {
  const [formData, setFormData] = useState(initialForm);
  const [errors, setErrors] = useState({});

  function handleChange(event) {
    const { name, value, type, checked } =
      event.target;

    setFormData((currentData) => ({
      ...currentData,
      [name]: type === "checkbox"
        ? checked
        : value
    }));
  }

  function validate() {
    const newErrors = {};

    if (!formData.name.trim()) {
      newErrors.name = "Name is required";
    }

    if (!formData.email.includes("@")) {
      newErrors.email =
        "Enter a valid email address";
    }

    if (formData.password.length < 8) {
      newErrors.password =
        "Password must be at least 8 characters";
    }

    if (!formData.role) {
      newErrors.role =
        "Please select a role";
    }

    return newErrors;
  }

  function handleSubmit(event) {
    event.preventDefault();

    const newErrors = validate();

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    setErrors({});

    console.log("Submitted:", formData);
  }

  function handleReset() {
    setFormData(initialForm);
    setErrors({});
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">
        Name
      </label>

      <input
        id="name"
        name="name"
        value={formData.name}
        onChange={handleChange}
      />

      {errors.name && (
        <p>{errors.name}</p>
      )}

      <label htmlFor="email">
        Email
      </label>

      <input
        id="email"
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
      />

      {errors.email && (
        <p>{errors.email}</p>
      )}

      <label htmlFor="password">
        Password
      </label>

      <input
        id="password"
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
      />

      {errors.password && (
        <p>{errors.password}</p>
      )}

      <label htmlFor="role">
        Role
      </label>

      <select
        id="role"
        name="role"
        value={formData.role}
        onChange={handleChange}
      >
        <option value="">
          Select a role
        </option>

        <option value="developer">
          Developer
        </option>

        <option value="designer">
          Designer
        </option>

        <option value="analyst">
          Data Analyst
        </option>
      </select>

      {errors.role && (
        <p>{errors.role}</p>
      )}

      <label>
        <input
          type="checkbox"
          name="newsletter"
          checked={formData.newsletter}
          onChange={handleChange}
        />

        Subscribe to newsletter
      </label>

      <button type="submit">
        Create Account
      </button>

      <button
        type="button"
        onClick={handleReset}
      >
        Reset
      </button>
    </form>
  );
}

This example demonstrates most of the important React form patterns in one component.

Image Placement: Complete React Form

Place this image before the complete React form example.

Create a modern 16:9 educational infographic titled “React Form Data Flow”. Show a polished registration form on one side and a React state object on the other. Visually show the flow Input → onChange → State → Validation → Submit. Include small visual indicators for controlled inputs, validation errors, and successful submission. Use a modern Web4Learners developer aesthetic with rounded cards, subtle gradients, clean typography, minimal text, and a contemporary UI design.

Common React Form Mistakes

Forgetting event.preventDefault()

If you handle form submission with React, remember:

function handleSubmit(event) {
  event.preventDefault();
}

when you do not want the browser’s default navigation behavior.

Using value Without onChange

This can make an input effectively read-only:

<input value={name} />

For a controlled editable input, provide an update mechanism:

<input
  value={name}
  onChange={(event) => setName(event.target.value)}
/>

Using value for a Checkbox

For checkboxes, use:

checked={accepted}

and:

event.target.checked

rather than treating the checked state as a text value.

Mutating Form State

Avoid:

formData.email = "test@example.com";

Instead:

setFormData((currentData) => ({
  ...currentData,
  email: "test@example.com"
}));

Forgetting name Attributes

If using one generic change handler for multiple fields, give each field a useful name:

<input
  name="email"
/>

Then:

const { name, value } = event.target;

can determine which property to update.

Using Array Index as a Dynamic Form Key

Avoid:

key={index}

when dynamic items can be removed, inserted, or reordered.

Prefer:

key={item.id}

with a stable identifier.

Resetting Only the UI but Not State

If an input is controlled by React state, resetting only the DOM does not reset the React state.

Reset the state itself:

setFormData(initialForm);

Validating Only in the Browser

Browser validation is useful for user experience, but data sent to a server should also be validated on the server.

Best Practices for React Forms

Give Every Important Field a Label

Use:

<label htmlFor="email">
  Email
</label>

and:

<input id="email" />

This improves accessibility and usability.

Keep Form State Organized

For related fields, an object can make state easier to manage:

const [formData, setFormData] = useState({
  name: "",
  email: "",
  phone: ""
});

For unrelated state, separate state variables may be clearer.

Keep Validation Logic Separate

For larger forms, putting validation in a function can make the component easier to read:

function validateForm(data) {
  // validation logic
}

Show Clear Error Messages

Instead of:

Invalid input

prefer a useful message such as:

Enter a valid email address.

Disable Submission When Appropriate

During an API request:

<button disabled={isSubmitting}>

This can prevent accidental duplicate submissions.

Keep State Updates Immutable

For objects:

setFormData((current) => ({
  ...current,
  email: newEmail
}));

For arrays:

setItems((current) => [
  ...current,
  newItem
]);

Use Stable Keys for Dynamic Fields

Stable IDs help React correctly preserve the identity of dynamic form elements.

React Forms Cheat Sheet

TaskReact Pattern
Text inputvalue + onChange
Textareavalue + onChange
Selectvalue + onChange
Checkboxchecked + onChange
Radiochecked + onChange
Submit formonSubmit
Prevent browser submissionevent.preventDefault()
Controlled inputReact state owns the value
Uncontrolled inputDOM owns the value
Initial uncontrolled valuedefaultValue
Initial checkbox statedefaultChecked
Form validationCheck values before submission
Error stateStore validation errors
Reset controlled formReset React state
Dynamic fieldsStore fields in an array
Dynamic field identityUse stable keys
API submissionUse async submission logic
Loading UITrack isSubmitting

Key Takeaways

  • React forms use normal HTML form elements with React event handling and state.
  • Inputs can be controlled by React state using value and onChange.
  • Checkboxes use checked and event.target.checked.
  • Radio buttons use the same name to form a group.
  • <textarea> and <select> can also be controlled through React state.
  • Multiple related inputs can be stored in an object.
  • A generic onChange handler can update multiple fields using the input’s name.
  • Form submission is commonly handled with onSubmit.
  • event.preventDefault() prevents the browser’s default form navigation when appropriate.
  • Controlled components give React direct control over form values.
  • Uncontrolled components allow the DOM to maintain their values and can be accessed with refs.
  • Validation should provide useful feedback instead of simply rejecting input.
  • Client-side validation improves the user experience, but server-side validation is still required for data sent to a backend.
  • Error messages should be connected clearly to their corresponding fields.
  • Controlled forms should reset their React state when you want to clear them.
  • Dynamic forms can use arrays of objects to represent fields.
  • Dynamic form items should have stable keys.
  • Form submission often needs loading, success, and error states.
  • Good form structure improves accessibility, maintainability, and user experience.

Leave a Comment