Forms in React
Forms in React: controlled inputs, form state objects, validation, checkboxes, radios, selects, submission, loading, and controlled vs uncontrolled patterns.
Forms in React
Forms in React are one of the foundational pieces of modern React engineering.
Forms are everywhere in real-world applications:
- Login and registration
- Search
- Contact forms
- Checkout
- User profiles
- Feedback and surveys
- Settings and authentication
- Data entry, filtering, and configuration
Unlike traditional HTML forms, React applications commonly use state to control form values and event handlers to manage user input and submission.
Learning Objectives
By the end of this lesson, you will understand:
- Controlled components and why they matter
- Managing one field vs one form state object
onSubmit,preventDefault(), and submission flows- Text, number, checkbox, radio, select, and textarea controls
- Validation, errors, loading, and reset patterns
- Controlled vs uncontrolled inputs and refs
- Common mistakes, best practices, and interview questions
1. What Is a Form?
A form is a collection of controls that allows users to enter or select information.
<form><input type="text" /><input type="email" /><button type="submit">Submit</button></form>
In React, we typically connect these controls to component state:
User enters data↓Input event↓Event handler↓State update↓React re-renders↓UI reflects current value
2. A Basic React Form
function LoginForm() {return (<form><input type="email" /><input type="password" /><button type="submit">Login</button></form>);}
This creates the form UI, but React is not yet managing the values. Introduce state to make the form interactive.
3. Controlled Components
The most important concept in React forms is the controlled component. A controlled input gets its value from React state.
import { useState } from "react";function LoginForm() {const [email, setEmail] = useState("");return (<inputtype="email"value={email}onChange={(event) => {setEmail(event.target.value);}}/>);}
value={email} means React controls the input's current value. onChange updates state when the user types.
User types↓onChange↓event.target.value↓setEmail()↓email state changes↓React renders↓input displays new value
4. Why Controlled Components Matter
Controlled inputs give React a single source of truth.
Without React state:Input → Browser owns valueWith a controlled component:React State → Input value ← onChange
This makes it easier to validate, disable buttons, show live feedback, reset forms, submit data, transform values, and display errors.
5. Handling Text Inputs
import { useState } from "react";function ProfileForm() {const [name, setName] = useState("");function handleNameChange(event) {setName(event.target.value);}return (<inputtype="text"value={name}onChange={handleNameChange}/>);}
The important pieces are event.target.value and value={name}.
6. Handling Multiple Inputs
One approach is separate state variables:
const [name, setName] = useState("");const [email, setEmail] = useState("");const [password, setPassword] = useState("");<inputvalue={name}onChange={(event) => setName(event.target.value)}/><inputvalue={email}onChange={(event) => setEmail(event.target.value)}/><inputvalue={password}onChange={(event) => setPassword(event.target.value)}/>
This is valid for smaller forms.
7. Managing Form Data with One State Object
For larger forms, keep related fields together:
const [form, setForm] = useState({name: "",email: "",password: ""});function handleChange(event) {const { name, value } = event.target;setForm({...form,[name]: value});}<inputname="name"value={form.name}onChange={handleChange}/><inputname="email"value={form.email}onChange={handleChange}/><inputname="password"value={form.password}onChange={handleChange}/>
name="email"↓event.target.name → "email"event.target.value → new value↓setForm()
8. Updating Objects in State
Preserve other fields with the spread operator:
setForm({...form,[name]: value});// User changes email:setForm({...form,email: "new@example.com"});// → { name: "Rahul", email: "new@example.com", password: "123456" }
9. Prefer Functional State Updates
When the next state depends on the previous state, a functional update is clearer and safer:
setForm((currentForm) => ({...currentForm,[name]: value}));
10. Form Submission
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>);}
11. Why Use preventDefault()?
Browsers may navigate or reload on submit. React apps usually handle submission themselves:
event.preventDefault();
Submit↓preventDefault()↓Validate↓Send API request↓Handle response↓Update UI
12. Complete Login Form
import { useState } from "react";function LoginForm() {const [form, setForm] = useState({email: "",password: ""});function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}function handleSubmit(event) {event.preventDefault();console.log(form);}return (<form onSubmit={handleSubmit}><inputtype="email"name="email"value={form.email}onChange={handleChange}placeholder="Email"/><inputtype="password"name="password"value={form.password}onChange={handleChange}placeholder="Password"/><button type="submit">Login</button></form>);}
13. Labels and Accessibility
Prefer linking labels with htmlFor and matching id:
<label htmlFor="email"></label><inputid="email"name="email"type="email"/>
label htmlFor↓input id
14. Input Types
<input type="text" /><input type="email" /><input type="password" /><input type="number" /><input type="date" /><input type="tel" /><input type="url" /><input type="search" />
Event handling is usually similar with onChange, but some controls expose different properties (e.g. checkboxes).
15. Handling Number Inputs
event.target.value is commonly a string (e.g. "25"). Convert intentionally when you need a number:
const age = Number(event.target.value);
Be careful: Number("") is 0. Sometimes keep the raw string and convert during validation or submission.
16. Handling Checkboxes
Checkboxes use checked rather than value for their checked state.
import { useState } from "react";function Settings() {const [notifications, setNotifications] = useState(false);function handleChange(event) {setNotifications(event.target.checked);}return (<label><inputtype="checkbox"checked={notifications}onChange={handleChange}/>Enable notifications</label>);}
Text input → event.target.valueCheckbox → event.target.checked
17. Multiple Checkboxes
Store selected values in an array:
const [skills, setSkills] = useState([]);<label><inputtype="checkbox"value="React"checked={skills.includes("React")}onChange={handleSkillChange}/>React</label>function handleSkillChange(event) {const { value, checked } = event.target;setSkills((currentSkills) => {if (checked) {return [...currentSkills, value];}return currentSkills.filter((skill) => skill !== value);});}
18. Radio Buttons
Radio buttons allow one choice from a group. Shared name groups them:
const [gender, setGender] = useState("");<label><inputtype="radio"name="gender"value="male"checked={gender === "male"}onChange={(event) => setGender(event.target.value)}/>Male</label><label><inputtype="radio"name="gender"value="female"checked={gender === "female"}onChange={(event) => setGender(event.target.value)}/>Female</label>
19. Select Dropdowns
const [country, setCountry] = useState("");<selectvalue={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">United Kingdom</option></select>
20. Textareas
Control <textarea> with value and onChange (not children text like older HTML):
const [message, setMessage] = useState("");function handleChange(event) {setMessage(event.target.value);}<textareavalue={message}onChange={handleChange}placeholder="Enter your message"/>
21. Form Validation
function handleSubmit(event) {event.preventDefault();if (!form.email) {console.log("Email is required");return;}if (!form.password) {console.log("Password is required");return;}console.log("Form is valid");}
Submit↓Validate↓┌─────────────┐│ │Invalid Valid│ │▼ ▼Show errors Submit
22. Storing Validation Errors
const [errors, setErrors] = useState({});function validate() {const newErrors = {};if (!form.email) {newErrors.email = "Email is required";}if (!form.password) {newErrors.password = "Password is required";}return newErrors;}function handleSubmit(event) {event.preventDefault();const newErrors = validate();setErrors(newErrors);if (Object.keys(newErrors).length > 0) {return;}console.log("Submit form");}
23. Displaying Validation Errors
<label htmlFor="email"></label><inputid="email"name="email"type="email"value={form.email}onChange={handleChange}/>{errors.email && (<p>{errors.email}</p>)}
This combines controlled inputs, state, event handling, conditional rendering, and validation.
24. Validation on Change
function handleEmailChange(event) {const value = event.target.value;setEmail(value);if (!value) {setEmailError("Email is required");} else {setEmailError("");}}
Validating every keystroke is not always best UX. Validate on change, blur, submit, or after submission attempts based on requirements.
25. Validation on Blur
onBlur fires when an input loses focus — useful so users finish typing before seeing errors:
<inputvalue={email}onChange={handleChange}onBlur={handleBlur}/>function handleBlur() {if (!email) {setError("Email is required");}}
26. Disabling the Submit Button
<buttontype="submit"disabled={!form.email || !form.password}>Login</button>
Email empty OR Password empty → Button disabledEmail ✓ and Password ✓ → Button enabled
27. Form Submission and API Calls
async function handleSubmit(event) {event.preventDefault();const response = await fetch("/api/login", {method: "POST",headers: {"Content-Type": "application/json"},body: JSON.stringify(form)});const data = await response.json();console.log(data);}
Form → Submit → Validate → API request → Server → Response → Update UI
28. Handling Loading State
const [isSubmitting, setIsSubmitting] = useState(false);async function handleSubmit(event) {event.preventDefault();setIsSubmitting(true);try {await saveData(form);} finally {setIsSubmitting(false);}}<buttontype="submit"disabled={isSubmitting}>{isSubmitting ? "Saving..." : "Save"}</button>
29. Complete Registration Form
import { useState } from "react";function RegistrationForm() {const [form, setForm] = useState({name: "",email: "",password: ""});const [errors, setErrors] = useState({});function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}function handleSubmit(event) {event.preventDefault();const newErrors = {};if (!form.name.trim()) newErrors.name = "Name is required";if (!form.email.trim()) newErrors.email = "Email is required";if (!form.password) newErrors.password = "Password is required";setErrors(newErrors);if (Object.keys(newErrors).length > 0) return;console.log("Registration data:", form);}return (<form onSubmit={handleSubmit}><div><label htmlFor="name">Name</label><input id="name" name="name" value={form.name} onChange={handleChange} />{errors.name && <p>{errors.name}</p>}</div><div><label htmlFor="email">Email</label><inputid="email"name="email"type="email"value={form.email}onChange={handleChange}/>{errors.email && <p>{errors.email}</p>}</div><div><label htmlFor="password">Password</label><inputid="password"name="password"type="password"value={form.password}onChange={handleChange}/>{errors.password && <p>{errors.password}</p>}</div><button type="submit">Register</button></form>);}
30. Resetting a Form
function resetForm() {setForm({name: "",email: "",password: ""});setErrors({});}
31. Form Submission Flow
User submits│▼preventDefault()│▼Validate│┌──────┴──────┐│ │Invalid Valid│ │▼ ▼Show errors Set loading│▼API request│┌─────────┴─────────┐│ │Success Error│ │▼ ▼Show success Show error│▼Reset / redirect
32. Controlled vs Uncontrolled Components
Controlled
React state owns the value:
<inputvalue={name}onChange={handleChange}/>
Uncontrolled
The DOM maintains the value; React can access it with a ref:
<input ref={inputRef} />
Prefer controlled when you need validation, conditional UI, resets, or other state-driven behavior.
33. Using Refs with Forms
import { useRef } from "react";function Form() {const inputRef = useRef(null);function handleSubmit(event) {event.preventDefault();console.log(inputRef.current.value);}return (<form onSubmit={handleSubmit}><input ref={inputRef} /><button type="submit">Submit</button></form>);}
Here the DOM holds the current value — React does not store it in state.
34. When to Use Controlled Inputs
Controlled inputs are especially useful when:
- The value affects other UI
- Validation is required
- You need to reset fields programmatically
- You disable/enable controls dynamically
- You show live character counts or transform values
- You need the current value elsewhere in the component
<p>Characters: {message.length}</p>
35. Common Mistakes
Mistake 1: Forgetting onChange
// Bad<input value={name} />// Better<inputvalue={name}onChange={handleChange}/>
Mistake 2: Forgetting preventDefault()
function handleSubmit(event) {event.preventDefault();console.log("Submitted");}
Mistake 3: Mutating the form object
// Avoidform.email = "new@example.com";// InsteadsetForm((currentForm) => ({...currentForm,email: "new@example.com"}));
Mistake 4: Forgetting name
With one generic handleChange, inputs need matching name attributes.
Mistake 5: Treating checkbox like text
Use event.target.checked for checked state, not event.target.value.
Mistake 6: Mixing controlled and uncontrolled accidentally
Be consistent — do not start without a value and later switch to a state-driven value.
36. Best Practices
- Use controlled components when the form needs React-driven behavior
- Keep form state organized (group related fields for larger forms)
- Use meaningful
nameattributes - Use labels with
htmlFor - Validate intentionally — do not overwhelm users
- Show submission/loading state and prevent duplicate submits
- Keep the flow: Input → State → Validation → Submit → Result
- Preserve state immutability
37. Practical Example — Contact Form
import { useState } from "react";function ContactForm() {const [form, setForm] = useState({name: "",email: "",message: ""});function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}function handleSubmit(event) {event.preventDefault();console.log("Contact form:", form);}return (<form onSubmit={handleSubmit}><div><label htmlFor="name">Name</label><input id="name" name="name" value={form.name} onChange={handleChange} /></div><div><label htmlFor="email">Email</label><inputid="email"name="email"type="email"value={form.email}onChange={handleChange}/></div><div><label htmlFor="message">Message</label><textareaid="message"name="message"value={form.message}onChange={handleChange}/></div><button type="submit">Send Message</button></form>);}
All three fields share one handleChange — a core React form pattern.
38. Event + State + Form Mental Model
USER│▼Form Control│▼onChange│▼Event Handler│▼setState()│▼React State│▼Component Render│▼Updated Form
Submit → onSubmit → preventDefault() → Validate → API / Logic → Success or Error → Update UI
39. Interview Questions
1. What is a controlled component?
A form element whose value is controlled by React state.
<inputvalue={name}onChange={handleChange}/>
2. What is an uncontrolled component?
The DOM maintains the form value, often accessed through a ref.
3. Why is preventDefault() used?
It prevents the browser's default form submission so the app can handle submission itself.
4. How do you handle multiple inputs with one function?
function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}
5. How do you handle a checkbox?
Use event.target.checked and control with checked={value}.
6. How do you handle a select?
<selectvalue={country}onChange={handleChange}>
7. How do you validate a form?
const errors = validate(form);if (Object.keys(errors).length > 0) {return;}
8. value vs defaultValue?
value for controlled inputs; defaultValue for initial uncontrolled values.
9. checked vs defaultChecked?
checked controls via React state; defaultChecked sets initial uncontrolled checkbox state.
10. Why not mutate form state directly?
Update through the setter so React can correctly schedule and reflect the change.
40. Practice Exercises
Exercise 1 — Name Form
Name field + Submit; display the entered name after submission.
Exercise 2 — Login Form
Controlled email, password, and Login; print submitted values to the console.
Exercise 3 — Registration Form
Name, email, password, confirm password — validate required fields and matching passwords.
Exercise 4 — Profile Form
Name, email, age, country, bio — one state object and one handleChange.
Exercise 5 — Checkbox
Accept terms checkbox; disable Submit until selected.
Exercise 6 — Radio Buttons
Gender selector with radio buttons; only one option selectable.
Exercise 7 — Skills
Multiple checkboxes (JavaScript, React, TypeScript, Node.js); store selections in an array.
Exercise 8 — Contact Form
Name, email, message with validation and error messages under invalid fields.
41. Quick Cheat Sheet
| Requirement | React Pattern |
|---|---|
| Text / textarea / select | value + onChange |
| Checkbox / radio | checked + onChange |
| Form submit | onSubmit |
| Prevent browser submit | event.preventDefault() |
| Input value | event.target.value |
| Checkbox state | event.target.checked |
| Field name | event.target.name |
| Controlled | React state owns value |
| Uncontrolled | DOM owns value |
| Initial uncontrolled | defaultValue / defaultChecked |
| Reset form | Update state to initial values |
Key Takeaway
React forms are built around:
Form Input → Event → State → Validation → Submission → Application Logic → Updated UI
The core pattern:
const [form, setForm] = useState({name: "",email: ""});function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}function handleSubmit(event) {event.preventDefault();// Validate and submit}<form onSubmit={handleSubmit}><inputname="name"value={form.name}onChange={handleChange}/><inputname="email"value={form.email}onChange={handleChange}/><button type="submit">Submit</button></form>
In a controlled React form, state is the source of truth, onChange keeps state synchronized with user input, and onSubmit handles the final form action.