Forms in React
Forms are how users provide input — registration, login, search, checkout, settings. HTML forms work fine on their own, but in React apps you usually want JavaScript to control what happens: validate inputs, show inline errors, disable the submit button while processing, and update state without a page reload. React provides a clean model for handling all of this.
HTML Forms vs React Forms
In plain HTML, a form collects input and sends it to a server when submitted — the browser handles everything and the page reloads. In React, you take over: you intercept the submit event, read the values from state (or the DOM), validate them, and decide what to do.
HTML forms — browser manages values, page reloads on submit, no JavaScript needed
React controlled forms — React state tracks every input value, instant validation, no page reload
React uncontrolled forms — DOM manages values, React reads them via refs on submit
Most React forms are controlled — state is the source of truth. The next page covers controlled components in depth. This page covers the fundamentals that apply to both approaches.
The Most Important Step: e.preventDefault()
Always call e.preventDefault() at the start of your onSubmit handler. Without it, the browser performs a full page reload when the form is submitted — which resets your React state and makes SPA navigation impossible.
function LoginForm() {
function handleSubmit(e) {
e.preventDefault() // MUST be first — stop the browser from reloading
console.log('Form submitted!')
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Log in</button>
</form>
)
}A Complete Basic Form
Here's a full contact form that tracks all field values in state, validates on submit, and resets after success:
import { useState } from 'react'
function ContactForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [message, setMessage] = useState('')
const [submitted, setSubmitted] = useState(false)
const [error, setError] = useState('')
function handleSubmit(e) {
e.preventDefault()
// Basic validation
if (!name.trim() || !email.trim() || !message.trim()) {
setError('All fields are required.')
return
}
if (!email.includes('@')) {
setError('Please enter a valid email address.')
return
}
// Submit the data
submitContactForm({ name, email, message })
.then(() => {
setSubmitted(true)
setName('')
setEmail('')
setMessage('')
setError('')
})
.catch(() => setError('Something went wrong. Please try again.'))
}
if (submitted) {
return <p>Thank you! We'll be in touch.</p>
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Your full name"
/>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@example.com"
/>
<label htmlFor="message">Message</label>
<textarea
id="message"
value={message}
onChange={e => setMessage(e.target.value)}
rows={4}
placeholder="How can we help?"
/>
<button type="submit">Send Message</button>
</form>
)
}The onChange Handler
The onChange event fires every time the input value changes — on every keystroke. Your handler reads e.target.value and updates the corresponding state:
// Long form — explicit handler function
function handleNameChange(e) {
setName(e.target.value)
}
<input value={name} onChange={handleNameChange} />
// Shorthand — inline arrow (equally correct and very common)
<input value={name} onChange={e => setName(e.target.value)} />
// For checkboxes, use e.target.checked instead of e.target.value
<input
type="checkbox"
checked={agreed}
onChange={e => setAgreed(e.target.checked)}
/>Accessibility: Always Use Labels
Every form input should have an associated <label>. This is crucial
for screen readers and also enlarges the click target. React uses
htmlFor instead of for (since for is a JavaScript reserved word):
// ✓ Explicit label with htmlFor + input id
<label htmlFor="username">Username</label>
<input id="username" type="text" value={username} onChange={...} />
// ✓ Implicit label (input wrapped inside label)
<label>
Username
<input type="text" value={username} onChange={...} />
</label>
// ✗ No label — screen readers can't identify the field
<input type="text" placeholder="Username" value={username} onChange={...} />Form Reset
Resetting a controlled form is simply setting all state values back to their initial values:
function SignupForm() {
const [form, setForm] = useState({ name: '', email: '', password: '' })
function handleReset() {
setForm({ name: '', email: '', password: '' })
}
function handleSubmit(e) {
e.preventDefault()
submitSignup(form).then(handleReset)
}
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" type="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
<button type="submit">Sign up</button>
<button type="button" onClick={handleReset}>Reset</button>
</form>
)
}Controlled vs Uncontrolled: Which to Use?
Controlled (React state tracks every value) — use for most forms; enables instant validation, conditional fields, derived values
Uncontrolled (DOM tracks values, read via ref on submit) — simpler for basic forms, better for integrating with non-React code or large file uploads
React Hook Form — a library that gives you uncontrolled performance with controlled ergonomics; the recommended choice for complex forms
React's Form Philosophy
React's approach to forms is deliberately explicit. Unlike frameworks with two-way binding directives (Angular's [(ngModel)], Vue's v-model), React makes data flow visible: state goes in, events come out. You always know exactly where form data lives and how it changes.
This verbosity pays off in larger applications: form state is just regular React state. You can derive values from it, share it between components, persist it to localStorage, or send it to a server — using the same patterns you use for any other state.