Every form in the MERN Blog โ login, register, create post, edit post โ needs to collect input from the user and send it to the Express API. React offers two approaches for managing form inputs: controlled components, where React state owns the input value and every keystroke updates state, and uncontrolled components, where the DOM owns the value and you read it via a ref when needed. Understanding the difference โ and knowing which pattern to use โ is the foundation of every form you will build in the MERN stack.
Controlled vs Uncontrolled โ Side by Side
| Controlled Component | Uncontrolled Component | |
|---|---|---|
| Who owns the value | React state (useState) |
The DOM |
| How to read the value | Read the state variable | Read via a ref (useRef) |
| Updates via | onChange โ setState |
No handler required โ DOM updates itself |
| Validation | Easy โ validate state on any change or submit | Harder โ must read ref to validate |
| Programmatic set | Easy โ call setState('') to clear |
Must set ref.current.value = '' |
| Recommended for MERN | โ Yes โ standard pattern | Only for simple one-off reads (file uploads, focus) |
value prop and an onChange handler. React then owns the value โ the input displays whatever state holds, and every keystroke calls onChange to update state. An uncontrolled input has no value prop โ the browser manages the value independently and you read it via a ref when you need it (typically on form submit). React’s official guidance recommends controlled components for most forms.value prop on an input without an onChange handler, React makes the input read-only and logs a warning. This is a common mistake when first learning controlled inputs. Always pair value with onChange. If you want a read-only input intentionally, use readOnly instead: <input value={savedValue} readOnly />.value is undefined) and then providing a value prop later causes a React warning: “A component is changing an uncontrolled input to be controlled.” Always initialise controlled inputs with a defined value โ use an empty string for text inputs, not undefined or null: useState('') not useState(null).Controlled Component โ Full Example
import { useState } from 'react';
function SearchInput({ onSearch }) {
// React owns the value
const [query, setQuery] = useState(''); // โ initialised to empty string, not null
const handleChange = (e) => {
setQuery(e.target.value); // every keystroke updates state
};
const handleSubmit = (e) => {
e.preventDefault();
onSearch(query); // read from state โ always current
};
const handleClear = () => {
setQuery(''); // programmatic clear โ easy with controlled
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query} {/* controlled: value comes from state */}
onChange={handleChange} {/* every keystroke syncs DOM โ state */}
placeholder="Search posts..."
/>
{query && <button type="button" onClick={handleClear}>ร</button>}
<button type="submit">Search</button>
</form>
);
}
Uncontrolled Component โ When to Use
import { useRef } from 'react';
// Uncontrolled: useful when you only need the value on submit
// and do not need live validation or programmatic updates
function SimpleSearchForm({ onSearch }) {
const inputRef = useRef(null); // ref points to the DOM input
const handleSubmit = (e) => {
e.preventDefault();
onSearch(inputRef.current.value); // read from DOM on submit
inputRef.current.value = ''; // clear by mutating the DOM ref
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef} {/* no value prop โ DOM owns value */}
placeholder="Search..."
defaultValue="" {/* initial value (not controlled) */}
/>
<button type="submit">Search</button>
</form>
);
}
// โโ When uncontrolled is appropriate โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โ File inputs: <input type="file" ref={fileRef} /> (file inputs cannot be controlled)
// โ One-off reads with no validation or live feedback needed
// โ Integrating with non-React DOM libraries that manage their own state
// โโ Controlled is better when โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โ Live validation (show errors as user types)
// โ Conditional disabling based on input value
// โ Transforming input (auto-capitalise, format phone number)
// โ Programmatic reset after form submission
// โ Pre-populating edit forms from fetched data
Generic onChange Handler for Multiple Fields
// One handler for all form fields using the input's name attribute
function LoginForm() {
const [formData, setFormData] = useState({ email: '', password: '' });
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
// Computed property key: uses the input's name attribute
[name]: type === 'checkbox' ? checked : value,
}));
};
return (
<form>
<input
name="email" {/* name must match the state key */}
type="email"
value={formData.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
<input
name="rememberMe"
type="checkbox"
checked={formData.rememberMe}
onChange={handleChange}
/>
</form>
);
}
Common Mistakes
Mistake 1 โ Setting value without onChange (accidental read-only)
โ Wrong โ controlled input with no way to update it:
<input value={email} />
{/* React warning: value prop without onChange โ input is read-only */}
โ Correct โ always pair value with onChange:
<input value={email} onChange={e => setEmail(e.target.value)} /> {/* โ */}
Mistake 2 โ Initialising controlled state with null or undefined
โ Wrong โ undefined value makes React treat input as uncontrolled initially:
const [email, setEmail] = useState(null); // null โ uncontrolled
// Warning: changing uncontrolled input to controlled
โ Correct โ initialise to empty string:
const [email, setEmail] = useState(''); {/* '' โ controlled from the start โ */}
Mistake 3 โ Forgetting to use the name attribute for the generic handler
โ Wrong โ name attribute missing means [undefined] key is set:
<input value={formData.email} onChange={handleChange} />
{/* e.target.name is '' โ setFormData sets { '': value } not { email: value } */}
โ Correct โ always match the name to the state key:
<input name="email" value={formData.email} onChange={handleChange} /> {/* โ */}
Quick Reference
| Pattern | Code |
|---|---|
| Controlled text input | <input value={val} onChange={e => setVal(e.target.value)} /> |
| Controlled checkbox | <input type="checkbox" checked={bool} onChange={e => setFlag(e.target.checked)} /> |
| Controlled select | <select value={val} onChange={e => setVal(e.target.value)}> |
| Controlled textarea | <textarea value={text} onChange={e => setText(e.target.value)} /> |
| Generic multi-field handler | setForm(prev => ({ ...prev, [e.target.name]: e.target.value })) |
| Uncontrolled ref | const ref = useRef(); ref.current.value |
| Reset controlled form | setFormData({ email: '', password: '' }) |