Lesson 9 of 12

Forms & User Input

You will learn how to build forms whose inputs are driven by state, using one state object and one change handler. This pattern covers most forms you will write.

JSX
import { useState } from 'react'

function SignupForm({ onSubmit }) {
  const [form, setForm] = useState({ email: '', password: '', plan: 'free', terms: false })
  const [errors, setErrors] = useState({})
  const [submitting, setSubmitting] = useState(false)

  // One handler for every field, keyed by the input's name attribute
  function handleChange(e) {
    const { name, value, type, checked } = e.target
    setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
  }

  function validate(values) {
    const next = {}
    if (!values.email.includes('@')) next.email = 'Enter a valid email'
    if (values.password.length < 8)  next.password = 'At least 8 characters'
    if (!values.terms)               next.terms = 'You must accept the terms'
    return next
  }

  async function handleSubmit(e) {
    e.preventDefault()                    // stop the browser from reloading the page
    const found = validate(form)
    setErrors(found)
    if (Object.keys(found).length > 0) return

    setSubmitting(true)
    try {
      await onSubmit(form)
      setForm({ email: '', password: '', plan: 'free', terms: false })
    } catch (err) {
      setErrors({ form: err.message })
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email"
             value={form.email} onChange={handleChange}
             aria-invalid={!!errors.email} />
      {errors.email && <p role="alert">{errors.email}</p>}

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password"
             value={form.password} onChange={handleChange} />
      {errors.password && <p role="alert">{errors.password}</p>}

      <select name="plan" value={form.plan} onChange={handleChange}>
        <option value="free">Free</option>
        <option value="pro">Pro</option>
      </select>

      <label>
        <input name="terms" type="checkbox"
               checked={form.terms} onChange={handleChange} />
        I accept the terms
      </label>
      {errors.terms && <p role="alert">{errors.terms}</p>}

      <button type="submit" disabled={submitting}>
        {submitting ? 'Creating...' : 'Create account'}
      </button>
      {errors.form && <p role="alert">{errors.form}</p>}
    </form>
  )
}
JSX
import { useState, useRef } from 'react'

// Controlled: React state is the value. Predictable, validates as you type.
function Controlled() {
  const [value, setValue] = useState('')
  return <input value={value} onChange={e => setValue(e.target.value.toUpperCase())} />
}

// Uncontrolled: the DOM keeps the value, you read it on submit. Less code,
// fewer renders — good for simple forms and for file inputs (which MUST be
// uncontrolled).
function Uncontrolled() {
  const fileRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    const data = new FormData(e.currentTarget)     // reads every named field
    console.log(Object.fromEntries(data))
    console.log(fileRef.current.files[0])
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" defaultValue="Untitled" />   {/* defaultValue, not value */}
      <input name="upload" type="file" ref={fileRef} />
      <button>Save</button>
    </form>
  )
}

// Dynamic field lists
function TagEditor() {
  const [tags, setTags] = useState([''])

  const update = (i, v) => setTags(tags.map((t, idx) => (idx === i ? v : t)))
  const remove = i => setTags(tags.filter((_, idx) => idx !== i))

  return (
    <>
      {tags.map((tag, i) => (
        <div key={i}>
          <input value={tag} onChange={e => update(i, e.target.value)} />
          <button type="button" onClick={() => remove(i)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => setTags([...tags, ''])}>Add tag</button>
    </>
  )
}
JSX
import { useState, useEffect, useMemo } from 'react'

// Debouncing: wait for the user to stop typing before doing expensive work
function useDebounced(value, delay = 300) {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(timer)      // cancel on every new keystroke
  }, [value, delay])

  return debounced
}

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounced(query, 400)

  useEffect(() => {
    if (debouncedQuery) onSearch(debouncedQuery)
  }, [debouncedQuery, onSearch])

  return (
    <input
      type="search"
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Search..."
      aria-label="Search"
    />
  )
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.