React Forms & Controlled Components
Almost every real-world web application needs forms.
A user might need to:
Enter their name
Enter their email
Choose a role
Enter a password
Select a preference
Submit the form
In vanilla JavaScript, we can access form elements directly from the DOM.
React takes a different approach.
Instead of letting the DOM be the primary source of truth, we can store form values in React State.
This pattern is called a Controlled Component.
๐ง What Is a Form in React?
A normal HTML form might look like:
<form>
<input type="text" />
<button type="submit">Submit</button>
</form>
In React, we can connect the input to state:
const [name, setName] = useState("");
Then:
<input
value={name}
onChange={event => setName(event.target.value)}
/>
Now React knows the current value of the input.
The flow becomes:
User types
โ
onChange
โ
event.target.value
โ
setName()
โ
State updates
โ
Input re-renders
โ๏ธ What Is a Controlled Component?
A controlled input is an input whose value is controlled by React state.
Example:
import { useState } from "react";
function App() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={event => setName(event.target.value)}
/>
);
}
Here:
value={name}
means React controls the input's value.
And:
onChange={event => setName(event.target.value)}
updates that state whenever the user types.
So:
React State
โ
Input
โ
User types
โ
onChange
โ
React State
This is called controlled input or controlled component.
๐ Why Controlled Components Matter
At first, you might think:
"Why not just read the input when the user submits?"
Because controlling inputs gives React access to the current value at all times.
That means we can easily:
Validate input
Disable buttons
Display live previews
Show character counts
Enable/disable fields
Transform values
Display error messages
Submit data to APIs
For example:
<p>{name.length}/50 characters</p>
Because React already knows the current value.
๐ Basic Text Input
Let's start with a simple name input.
import { useState } from "react";
function App() {
const [name, setName] = useState("");
function handleNameChange(event) {
setName(event.target.value);
}
return (
<div>
<label>Name:</label>
<input
type="text"
value={name}
onChange={handleNameChange}
/>
<p>Your name: {name}</p>
</div>
);
}
export default App;
If the user types:
Saurabh
the state becomes:
"Saurabh"
and the UI updates immediately.
๐ฏ event.target.value
This is one of the most important things to remember.
For:
<input onChange={handleChange} />
we can write:
function handleChange(event) {
console.log(event.target.value);
}
If the user types:
React
then:
event.target.value
contains:
"React"
This is how we retrieve what the user entered.
๐ง Handling Multiple Inputs
Real forms usually have multiple fields.
For example:
Name
Email
Password
We could create separate state variables:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
Then:
<input
value={name}
onChange={event => setName(event.target.value)}
/>
<input
value={email}
onChange={event => setEmail(event.target.value)}
/>
<input
value={password}
onChange={event => setPassword(event.target.value)}
/>
This works perfectly well.
But for larger forms, there's another useful pattern.
๐ฆ Managing Form Data With One Object
Instead of:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
we can use:
const [formData, setFormData] = useState({
name: "",
email: "",
password: ""
});
Now all form data lives inside one object.
๐ Updating Object State
Suppose the user changes the name.
We can write:
setFormData({
...formData,
name: event.target.value
});
The spread operator keeps the existing fields.
For example:
{
name: "Saurabh",
email: "",
password: ""
}
If the email changes:
setFormData({
...formData,
email: event.target.value
});
The other values remain unchanged.
๐ง Dynamic Input Handling With name
We can make this much cleaner.
Give each input a name:
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
Then create one handler:
function handleChange(event) {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value
});
}
This is a very useful React form pattern.
๐ Understanding [name]: value
This syntax:
[name]: value
is a computed property name in JavaScript.
Suppose:
name = "email";
value = "test@example.com";
Then:
{
[name]: value
}
becomes:
{
email: "test@example.com"
}
If:
name = "username";
it becomes:
{
username: "..."
}
This allows one event handler to manage multiple fields.
๐ Complete Multi-Input Form
import { useState } from "react";
function RegistrationForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: ""
});
function handleChange(event) {
const { name, value } = event.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}
function handleSubmit(event) {
event.preventDefault();
console.log(formData);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">
Register
</button>
</form>
);
}
export default RegistrationForm;
Notice the pattern:
Input
โ
name + value
โ
handleChange
โ
setFormData()
โ
State
This pattern is worth memorizing.
๐ค Handling Form Submission
Forms use:
onSubmit
Example:
<form onSubmit={handleSubmit}>
Handler:
function handleSubmit(event) {
event.preventDefault();
console.log("Submitted!");
}
The:
event.preventDefault();
prevents the browser's default form submission behavior.
Now React can handle the submission.
๐ Why Use preventDefault()?
Without it, the browser may perform its normal form submission behavior.
In a React single-page application, we usually want to:
Collect data
โ
Validate data
โ
Send API request
โ
Show result
instead of allowing the browser to navigate/reload as part of the default submission.
๐ Password Input
Password fields work exactly like normal controlled inputs:
const [password, setPassword] = useState("");
Then:
<input
type="password"
value={password}
onChange={event => setPassword(event.target.value)}
/>
We can later combine this with conditional rendering to create:
Password: ********
[ Show Password ]
๐ Textarea
A <textarea> can also be controlled.
const [message, setMessage] = useState("");
Then:
<textarea
value={message}
onChange={event => setMessage(event.target.value)}
/>
Example:
function MessageForm() {
const [message, setMessage] = useState("");
return (
<div>
<textarea
value={message}
onChange={event => setMessage(event.target.value)}
placeholder="Write a message..."
/>
<p>{message.length} characters</p>
</div>
);
}
๐ Checkbox
Checkboxes are slightly different.
Instead of:
value
we generally control:
checked
Example:
const [accepted, setAccepted] = useState(false);
Then:
<input
type="checkbox"
checked={accepted}
onChange={event => setAccepted(event.target.checked)}
/>
Notice:
event.target.checked
rather than:
event.target.value
๐ง Why checked?
A checkbox has a boolean state:
true
false
So:
checked={accepted}
makes more semantic sense.
Example:
<label>
<input
type="checkbox"
checked={accepted}
onChange={event => setAccepted(event.target.checked)}
/>
I accept the terms
</label>
Now accepted will be either:
true
or:
false
๐ Radio Buttons
Radio buttons work similarly.
Suppose we want users to select a role:
โ Frontend Developer
โ Backend Developer
โ Full Stack Developer
State:
const [role, setRole] = useState("");
Then:
<label>
<input
type="radio"
name="role"
value="frontend"
checked={role === "frontend"}
onChange={event => setRole(event.target.value)}
/>
Frontend Developer
</label>
Another:
<label>
<input
type="radio"
name="role"
value="backend"
checked={role === "backend"}
onChange={event => setRole(event.target.value)}
/>
Backend Developer
</label>
The state stores the selected value.
๐ฝ Select Dropdown
A <select> can also be controlled.
const [country, setCountry] = useState("");
Then:
<select
value={country}
onChange={event => setCountry(event.target.value)}
>
<option value="">Select Country</option>
<option value="india">India</option>
<option value="usa">USA</option>
<option value="uk">UK</option>
</select>
Now:
User selects India
โ
onChange
โ
event.target.value
โ
setCountry("india")
๐งฉ Form Validation
Forms shouldn't blindly accept anything.
We can validate data before submitting.
For example:
function handleSubmit(event) {
event.preventDefault();
if (!formData.name) {
alert("Name is required");
return;
}
if (!formData.email) {
alert("Email is required");
return;
}
console.log("Form submitted!");
}
The logic becomes:
Submit
โ
Validate
โ
Invalid?
โโโ Yes โ Show error
โโโ No โ Submit
โ Displaying Validation Errors
Instead of using alert(), we can store errors in state.
const [error, setError] = useState("");
Then:
if (!formData.name) {
setError("Name is required");
return;
}
Display:
{error && <p>{error}</p>}
This uses the conditional rendering concept we learned on Day 65.
๐ฅ Multiple Validation Errors
For larger forms, we can store errors as an object:
const [errors, setErrors] = useState({});
Example:
setErrors({
name: "Name is required",
email: "Invalid email"
});
Then:
{errors.name && (
<p>{errors.name}</p>
)}
and:
{errors.email && (
<p>{errors.email}</p>
)}
This gives us much more control.
๐ง Validation + Conditional Rendering
Suppose:
{errors.email && (
<span>{errors.email}</span>
)}
If:
errors.email
contains:
"Invalid email"
we show the message.
If it doesn't exist:
undefined
nothing is displayed.
This is another example of combining concepts from previous days.
๐ข Input Validation Example
We can validate an email:
function validateEmail(email) {
return email.includes("@");
}
Then:
if (!validateEmail(formData.email)) {
setErrors({
email: "Please enter a valid email"
});
return;
}
For production applications, validation requirements are usually more comprehensive, but this demonstrates the basic idea.
๐ซ Disabling Submit Buttons
We can prevent submission until required fields are filled.
For example:
<button
type="submit"
disabled={!formData.email || !formData.password}
>
Login
</button>
If either field is empty, the button is disabled.
This is a great example of:
State
โ
Condition
โ
UI behavior
๐ Resetting a Form
After successful submission, we may want to clear the form.
setFormData({
name: "",
email: "",
password: ""
});
For example:
function handleSubmit(event) {
event.preventDefault();
console.log(formData);
setFormData({
name: "",
email: "",
password: ""
});
}
Now the inputs return to their initial values.
๐ง Controlled vs Uncontrolled Components
There are two broad approaches.
Controlled
React state controls the value:
<input
value={name}
onChange={handleChange}
/>
Uncontrolled
The DOM keeps track of the value, often accessed through a ref.
For example:
<input ref={inputRef} />
For many everyday React forms, controlled components are a straightforward and predictable approach.
We'll learn more about refs later.
๐ Controlled Component Mental Model
Remember this:
React State
โ
value
โ
Input
โ
User types
โ
onChange
โ
setState(...)
โ
React State
React stays in control of the data.
๐ฅ Complete Registration Form
Let's put everything together.
import { useState } from "react";
function RegistrationForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
role: ""
});
const [error, setError] = useState("");
function handleChange(event) {
const { name, value } = event.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}
function handleSubmit(event) {
event.preventDefault();
if (!formData.name) {
setError("Name is required");
return;
}
if (!formData.email) {
setError("Email is required");
return;
}
if (!formData.password) {
setError("Password is required");
return;
}
setError("");
console.log("Registration successful!");
console.log(formData);
}
return (
<form onSubmit={handleSubmit}>
<h1>Create Account</h1>
{error && (
<p>{error}</p>
)}
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<select
name="role"
value={formData.role}
onChange={handleChange}
>
<option value="">Select Role</option>
<option value="frontend">
Frontend Developer
</option>
<option value="backend">
Backend Developer
</option>
<option value="fullstack">
Full Stack Developer
</option>
</select>
<button type="submit">
Register
</button>
</form>
);
}
export default RegistrationForm;
This single example uses:
useStateControlled inputs
onChangeonSubmitevent.targetComputed property names
Object state
Conditional rendering
Validation
Select inputs
Error handling
Form reset concepts
๐ Best Practices
1. Give inputs meaningful name attributes
name="email"
This makes generic handlers much easier.
2. Keep form state predictable
For example:
const [formData, setFormData] = useState({
name: "",
email: "",
password: ""
});
3. Use functional state updates for object state
Prefer:
setFormData(prev => ({
...prev,
[name]: value
}));
This clearly updates based on the previous state.
4. Validate before submitting
Don't assume the user entered valid data.
5. Give useful error messages
Instead of:
Error
prefer:
Please enter a valid email address.
6. Keep validation logic separate when it gets complex
As applications grow, move validation into dedicated functions or utilities.
7. Don't store unnecessary derived state
For example, if you can calculate:
const isFormValid =
formData.email !== "" &&
formData.password !== "";
you don't necessarily need another state variable just for isFormValid.
๐ก My Biggest Takeaway
Today's biggest takeaway is:
A controlled form is essentially a conversation between React State and the user.
The user types:
User Input
โ
onChange
โ
State
โ
UI
And when the user submits:
Submit
โ
onSubmit
โ
Validate
โ
Process Data
โ
API / Backend
This is extremely important because forms are the primary way users provide data to our applications.
And soon, when we connect React to a backend, this same form data can be sent to our Node.js / Express APIs.
