Frontend Development 16 min read

Mastering React Forms: When to Use Controlled vs Uncontrolled Components

This article explains modern best practices for building forms in React, compares controlled and uncontrolled approaches, shows how to mix them, discusses server‑side components, validation, error handling, and recommends using FormData over useRef for cleaner, more performant code.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Mastering React Forms: When to Use Controlled vs Uncontrolled Components

Controlled vs Uncontrolled

Understanding the key difference between

controlled

and

uncontrolled

forms in React is essential. Controlled forms store each input's value in

state

and update the UI via the

value

prop, while uncontrolled forms let the browser manage the values and access them through native

<form>

and JavaScript APIs.

Typical controlled form example:

<code>import React, { useState } from 'react'

function ControlledForm() {
  const [value, setValue] = useState('');

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const handleSubmit = () => {
    sendInputValueToApi(value).then(() => {/* business logic */});
  };

  return (
    <>
      <input type="text" value={value} onChange={handleChange} />
      <button onClick={handleSubmit}>send</button>
    </>
  );
}
</code>

Controlled forms give fine‑grained control (e.g., custom validation, formatting) but can become verbose and cause unnecessary re‑renders when the form grows.

Uncontrolled form example:

<code>function UncontrolledForm() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const inputValue = formData.get('inputName');
    sendInputValueToApi(inputValue).then(() => {/* business logic */});
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="inputName" />
      <button type="submit">Send</button>
    </form>
  );
}
</code>

Uncontrolled forms reduce boilerplate and avoid managing a large

state

object, but direct access to individual input values for custom validation can be trickier.

Cautions

Do not rely on

useRef

for every input in an uncontrolled form; prefer the standard

new FormData()

API, which is widely supported and keeps code simpler.

Mixing Controlled and Uncontrolled

In many scenarios you may need a controlled input for special handling (e.g., phone‑number formatting) while keeping the rest of the form uncontrolled. Use

new FormData(...)

for submission and

state

only for the inputs that require live feedback.

<code>function MixedForm() {
  const [phoneNumber, setPhoneNumber] = useState('');

  const handlePhoneNumberChange = (event) => {
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    for (let [key, value] of formData.entries()) {
      console.log(`${key}: ${value}`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" />
      <label>Email:</label>
      <input type="email" name="email" />
      <label>Phone Number:</label>
      <input type="tel" name="phoneNumber" value={phoneNumber} onChange={handlePhoneNumberChange} />
      <label>Address:</label>
      <input type="text" name="address" />
      <button type="submit">Submit</button>
    </form>
  );
}

function formatPhoneNumber(number) {
  return number.replace(/\D/g, "").slice(0, 10);
}
</code>

How to Validate Uncontrolled Inputs

Native HTML validation attributes (required, pattern, minlength, etc.) can handle many cases without JavaScript. For custom validation, process the

FormData

inside the

onSubmit

handler.

Error Handling

Prefer handling validation and error messages in the

onSubmit

function of an uncontrolled form to avoid excessive state updates. Example:

<code>function UncontrolledForm() {
  const [errors, setErrors] = useState({});

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    let validationErrors = {};
    const email = formData.get('email');
    if (email && !email.endsWith('@example.com')) {
      validationErrors.email = 'Email must be from the domain example.com.';
    }
    if (formData.get('phoneNumber').length !== 10) {
      validationErrors.phoneNumber = 'Phone number must be 10 digits.';
    }
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
    } else {
      console.log(Array.from(formData.entries()));
      setErrors({});
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>Name:</label>
      <input type="text" name="name" required />
      {errors.name && <div>{errors.name}</div>}
      <label>Email (must be @example.com):</label>
      <input type="email" name="email" required />
      {errors.email && <div>{errors.email}</div>}
      <label>Phone Number (10 digits):</label>
      <input type="tel" name="phoneNumber" required pattern="\d{10}" />
      {errors.phoneNumber && <div>{errors.phoneNumber}</div>}
      <button type="submit">Submit</button>
    </form>
  );
}
</code>

Forms in Server Components

React Server Components (RSC) can render forms on the server, reducing the amount of JavaScript sent to the client. Uncontrolled forms work without client‑side JavaScript, allowing early interaction. When mixing controlled inputs, extract them into client components.

<code>// page.jsx
import { PhoneInput } from "./PhoneInput";

export default function Page() {
  async function create(formData) {
    "use server";
    // ...use the FormData
  }

  return (
    <form action={create}>
      <label>Name:</label>
      <input type="text" name="name" />
      <label>Email:</label>
      <input type="email" name="email" />
      <label>Phone Number:</label>
      <PhoneInput />
      <label>Address:</label>
      <input type="text" name="address" />
      <button type="submit">Submit</button>
    </form>
  );
}

// PhoneInput.jsx
"use client";
import { useState } from "react";

function formatPhoneNumber(number) {
  return number.replace(/\D/g, "").slice(0, 10);
}

export const PhoneInput = () => {
  const [phoneNumber, setPhoneNumber] = useState("");
  const handlePhoneNumberChange = (event) => {
    const formattedNumber = formatPhoneNumber(event.target.value);
    setPhoneNumber(formattedNumber);
  };
  return (
    <input type="tel" name="phoneNumber" value={phoneNumber} onChange={handlePhoneNumberChange} />
  );
};
</code>

Form Libraries

Popular libraries such as React Hook Form , Formik , and Informed focus on controlled forms. The author prefers uncontrolled forms to avoid extra state‑management dependencies.

Summary, Comparison, and Recommendations

Search results for "react forms" often point to outdated or misleading articles. The author argues that neither controlled nor uncontrolled is universally superior; a hybrid approach is usually best. For most cases, use uncontrolled forms with

new FormData()

in

onSubmit

, reserve

useRef

for specific scenarios like focusing fields, and keep state updates minimal to improve performance.

Hope this guide helps you build better React forms!

reactbest practicesFormsServer ComponentsControlled ComponentsUncontrolled Components
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.