ReactForms in React

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.

JSX
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:

JSX
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:

JSX
// 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):

JSX
// ✓ 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={...} />
Note
`placeholder` text is not a substitute for a label. Placeholders disappear when the user starts typing, and they have poor contrast by default. Always include a visible label.
Form Reset

Resetting a controlled form is simply setting all state values back to their initial values:

JSX
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

Warning
Never mix controlled and uncontrolled for the same input. An input is controlled if you pass a `value` prop. If you pass `value` but omit `onChange`, React will warn that the input is read-only. Either provide both `value` and `onChange`, or use `defaultValue` (uncontrolled) with no `value` prop.
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.

Tip
For simple forms (2-3 fields, minimal validation), the pattern shown on this page is fine. For complex forms — many fields, conditional fields, complex validation, multi-step flows — reach for React Hook Form. It dramatically reduces boilerplate while keeping the controlled component model.