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.
Controlled vs Uncontrolled
Understanding the key difference between
controlledand
uncontrolledforms in React is essential. Controlled forms store each input's value in
stateand update the UI via the
valueprop, 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
stateobject, but direct access to individual input values for custom validation can be trickier.
Cautions
Do not rely on
useReffor 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
stateonly 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
FormDatainside the
onSubmithandler.
Error Handling
Prefer handling validation and error messages in the
onSubmitfunction 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
useReffor specific scenarios like focusing fields, and keep state updates minimal to improve performance.
Hope this guide helps you build better React forms!
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.