Forms are everywhere in web applications - login screens, registration pages, search boxes, settings panels, and more. In React, forms work a bit differently than in HTML. You'll learn about controlled components, where React state controls form inputs, giving you complete power over form behavior. You'll also learn efficient patterns for handling multiple inputs, form submission, and building complex forms that scale. Let's master React forms!
Controlled Components
In React, form inputs should be controlled components - their values are controlled by React state:
Uncontrolled (HTML Way)
<!-- HTML - DOM is the source of truth -->
<input type="text" id="username" />
<script>
// Read value from DOM when needed
const username = document.getElementById('username').value
</script>
<!-- Problem: React doesn't know about changes
Can't easily validate, transform, or sync with other components -->Controlled (React Way)
function Form() {
// React state is the source of truth
const [username, setUsername] = React.useState('')
return (
<input
type="text"
value={username} // React controls the value
onChange={(e) => setUsername(e.target.value)} // Update state on change
/>
)
}
// Benefits:
// - React knows the value at all times
// - Easy to validate, transform, or sync
// - Can clear input: setUsername('')
// - Can set programmatically: setUsername('admin')Controlled Component Rules:
- Store value in state
- Set input's
valueprop to state - Update state in
onChangehandler - State is the single source of truth
Basic Form Inputs
Text Input
function TextInput() {
const [name, setName] = React.useState('')
return (
<div>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<p>You entered: {name}</p>
</div>
)
}Email Input
function EmailInput() {
const [email, setEmail] = React.useState('')
return (
<div>
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
/>
</label>
</div>
)
}Password Input
function PasswordInput() {
const [password, setPassword] = React.useState('')
const [showPassword, setShowPassword] = React.useState(false)
return (
<div>
<label>
Password:
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<button type="button" onClick={() => setShowPassword(!showPassword)}>
{showPassword ? 'Hide' : 'Show'}
</button>
</div>
)
}Number Input
function NumberInput() {
const [age, setAge] = React.useState(0)
const handleChange = (e) => {
// Convert to number
setAge(Number(e.target.value))
}
return (
<div>
<label>
Age:
<input
type="number"
value={age}
onChange={handleChange}
min="0"
max="120"
/>
</label>
</div>
)
}Textarea
function TextareaInput() {
const [message, setMessage] = React.useState('')
return (
<div>
<label>
Message:
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={4}
placeholder="Enter your message..."
/>
</label>
<p>Character count: {message.length}</p>
</div>
)
}
// Note: In React, textarea uses value prop (not children like HTML)Checkboxes and Radio Buttons
Single Checkbox
function SingleCheckbox() {
const [isChecked, setIsChecked] = React.useState(false)
return (
<label>
<input
type="checkbox"
checked={isChecked} // Use 'checked', not 'value'
onChange={(e) => setIsChecked(e.target.checked)}
/>
I agree to the terms and conditions
</label>
)
}
// IMPORTANT: Checkboxes use 'checked' prop and e.target.checkedMultiple Checkboxes
function MultipleCheckboxes() {
const [interests, setInterests] = React.useState([])
const handleChange = (interest) => {
if (interests.includes(interest)) {
// Remove if already selected
setInterests(interests.filter(i => i !== interest))
} else {
// Add if not selected
setInterests([...interests, interest])
}
}
return (
<div>
<p>Select your interests:</p>
<label>
<input
type="checkbox"
checked={interests.includes('coding')}
onChange={() => handleChange('coding')}
/>
Coding
</label>
<label>
<input
type="checkbox"
checked={interests.includes('design')}
onChange={() => handleChange('design')}
/>
Design
</label>
<label>
<input
type="checkbox"
checked={interests.includes('music')}
onChange={() => handleChange('music')}
/>
Music
</label>
<p>Selected: {interests.join(', ')}</p>
</div>
)
}Radio Buttons
function RadioButtons() {
const [size, setSize] = React.useState('medium')
return (
<div>
<p>Select size:</p>
<label>
<input
type="radio"
name="size"
value="small"
checked={size === 'small'}
onChange={(e) => setSize(e.target.value)}
/>
Small
</label>
<label>
<input
type="radio"
name="size"
value="medium"
checked={size === 'medium'}
onChange={(e) => setSize(e.target.value)}
/>
Medium
</label>
<label>
<input
type="radio"
name="size"
value="large"
checked={size === 'large'}
onChange={(e) => setSize(e.target.value)}
/>
Large
</label>
<p>Selected: {size}</p>
</div>
)
}
// Radio buttons: all have same 'name', different 'value'Select Dropdown
Basic Select
function SelectBasic() {
const [country, setCountry] = React.useState('us')
return (
<div>
<label>
Country:
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
</select>
</label>
<p>Selected: {country}</p>
</div>
)
}
// Select uses value prop on <select>, not on <option>Multi-Select
function MultiSelect() {
const [languages, setLanguages] = React.useState([])
const handleChange = (e) => {
const options = Array.from(e.target.selectedOptions)
const values = options.map(option => option.value)
setLanguages(values)
}
return (
<div>
<label>
Languages:
<select multiple value={languages} onChange={handleChange}>
<option value="js">JavaScript</option>
<option value="py">Python</option>
<option value="rb">Ruby</option>
<option value="go">Go</option>
</select>
</label>
<p>Selected: {languages.join(', ')}</p>
<p className="note">Hold Ctrl/Cmd to select multiple</p>
</div>
)
}Handling Multiple Inputs
Pattern 1: Individual State Variables
function Form() {
const [firstName, setFirstName] = React.useState('')
const [lastName, setLastName] = React.useState('')
const [email, setEmail] = React.useState('')
const handleSubmit = (e) => {
e.preventDefault()
console.log({ firstName, lastName, email })
}
return (
<form onSubmit={handleSubmit}>
<input
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First Name"
/>
<input
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last Name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
)
}
// Simple but verbose for many inputsPattern 2: Single State Object (Recommended)
function Form() {
const [formData, setFormData] = React.useState({
firstName: '',
lastName: '',
email: '',
age: 0,
subscribe: false
})
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
})
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(formData)
}
return (
<form onSubmit={handleSubmit}>
<input
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="First Name"
/>
<input
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Last Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
/>
<label>
<input
name="subscribe"
type="checkbox"
checked={formData.subscribe}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
<button type="submit">Submit</button>
</form>
)
}
// Single handler for all inputs - scales well!
// Key: Each input needs a 'name' attribute💡 Generic Handler Pattern
The generic handler pattern works because:
1. Each input has a name attribute matching the state key
2. We use [name] computed property to update the right key
3. We check type to handle checkboxes differently
This pattern is perfect for forms with many inputs!
Form Submission
function LoginForm() {
const [formData, setFormData] = React.useState({
email: '',
password: '',
rememberMe: false
})
const [isSubmitting, setIsSubmitting] = React.useState(false)
const handleChange = (e) => {
const { name, value, type, checked } = e.target
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
})
}
const handleSubmit = async (e) => {
e.preventDefault() // ALWAYS prevent default!
setIsSubmitting(true)
try {
// Simulate API call
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
console.log('Login successful!')
// Clear form after success
setFormData({ email: '', password: '', rememberMe: false })
} catch (error) {
console.error('Login failed:', error)
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
required
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
required
/>
<label>
<input
name="rememberMe"
type="checkbox"
checked={formData.rememberMe}
onChange={handleChange}
/>
Remember me
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
// Key points:
// 1. Always use e.preventDefault()
// 2. Disable button during submission
// 3. Show loading state
// 4. Handle errors
// 5. Clear form after successInteractive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
Common Form Patterns
Clearing Form After Submit
function Form() {
const initialState = { name: '', email: '' }
const [formData, setFormData] = React.useState(initialState)
const handleSubmit = (e) => {
e.preventDefault()
console.log(formData)
// Clear form
setFormData(initialState)
}
}Disabling Submit When Invalid
function Form() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const isValid = email.length > 0 && password.length >= 8
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit" disabled={!isValid}>
Submit
</button>
</form>
)
}Trimming Input on Blur
function Form() {
const [name, setName] = React.useState('')
const handleBlur = () => {
setName(name.trim()) // Remove whitespace when user leaves input
}
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={handleBlur}
/>
)
}Key Takeaways
- Use controlled components - state controls input values
- Set
valueprop andonChangehandler on inputs - Use
checkedprop for checkboxes/radios, notvalue - Always call
event.preventDefault()inonSubmit - Store form data in single state object for multiple inputs
- Use generic handler with
nameattributes - Check
typeto handle checkboxes vs text inputs - Disable submit button during submission
- Show loading state during async operations
- Clear form after successful submission
- Textarea uses
valueprop in React (not children) - Select uses
valueon the select element - Controlled components enable validation and transformation
What's Next?
You now know how to build forms in React! You can handle all types of inputs, manage form state efficiently, and submit forms properly.
But forms aren't complete without validation. In the next lesson, you'll learn about Form Validation:
- Real-time validation patterns
- Validation on blur vs on change
- Displaying error messages
- Custom validation rules
- Form-level vs field-level validation
Get ready to build bulletproof forms with proper validation! ✅