React: Forms and Controlled Components

Last updated: 2026-08-26

Forms serve as the bridge between web applications and users. In React, forms are no longer just about "filling them out and submitting them"—instead, "React knows every character you type." This pattern is called a controlled component.


1. What You'll Learn



2. The Evolution of a Registration Form

(1) Pain Point: Difficulty Tracking Data from Uncontrolled Forms

Alice created a user registration form that retrieves data using native methods:

HTML
<!-- HTML Form:Read data all at once upon submission -->
<form id="register-form">
  <input name="username" id="username" />
  <input type="email" name="email" id="email" />
  <input type="password" name="password" id="password" />
  <select name="role" id="role">
    <option value="developer">Developer</option>
    <option value="designer">Designer</option>
  </select>
  <button type="submit">Register</button>
</form>

<script>
  document.getElementById('register-form').addEventListener('submit', function(e) {
    e.preventDefault()
    // I'll go when I submit it. DOM Data from Zhongna
    const username = document.getElementById('username').value
    const email = document.getElementById('email').value
    const password = document.getElementById('password').value
    const role = document.getElementById('role').value
    // ... Manually verify each field
  })
</script>

Question:

(2) Solutions for React Controlled Components

JSX
function RegisterForm() {
  const [form, setForm] = React.useState({
    username: '',
    email: '',
    password: '',
    role: 'developer'
  })

  // Each input change React Everyone knows
  function handleChange(e) {
    const { name, value } = e.target
    setForm(prev => ({ ...prev, [name]: value }))
  }

  function handleSubmit(e) {
    e.preventDefault()
    console.log('Registration Data:', form)  // Retrieve the complete form data
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="password" value={form.password} onChange={handleChange} />
      <select name="role" value={form.role} onChange={handleChange}>
        <option value="developer">Developer</option>
        <option value="designer">Designer</option>
      </select>
      <button type="submit">Register</button>
    </form>
  )
}
▶ Try it Yourself

Benefits: React tracks each input field in real time, providing real-time validation and real-time data synchronization, and retrieves the complete data immediately upon submission.



3. Controlled Components vs. Uncontrolled Components

100%
graph TB
    subgraph "Controlled Components(Recommendations)"
        A1[User Input] --> B1[onChange Event]
        B1 --> C1[Update State]
        C1 --> D1[React Render Again]
        D1 --> E1[value = State]
    end
    
    subgraph "Uncontrolled Components"
        A2[User Input] --> B2[Update Directly DOM]
        B2 --> C2[Use when submitting ref Reading]
    end
    
    style C1 fill:#52c41a,color:#fff
    style B2 fill:#faad14,color:#000
Dimension Controlled Components Uncontrolled Components
Data Source React State (Single Source of Truth) The DOM Itself
Access Method Read directly from State Requires a ref or document.getElementById
Real-time validation ✅ Natively supported ❌ Requires additional monitoring
Instant Synchronization ✅ Real-time data synchronization ❌ Manual synchronization required
Use Cases Most forms File uploads, simple one-time forms

Rule: Use controlled components in 90% of cases. Only the file <input type="file"> must use an uncontrolled component.



4. Controlled Syntax for Various Form Elements

Form Element Bound Property onChange Value Example
<input type="text"> value e.target.value <input value={name} onChange={e => setName(e.target.value)} />
<input type="number"> value Number(e.target.value) Must be converted to numeric values manually
<input type="checkbox"> checked e.target.checked Boolean, not value
<input type="radio"> checked e.target.value Grouped by name; "checked" controls whether they are selected
<textarea> value e.target.value React uses value, not child text
<select> value e.target.value Multiple selections multiple + array
<input type="file"> ❌ Uncontrolled e.target.files[0] Can only be used as uncontrolled

(1) Text Input

JSX
function TextInputs() {
  const [name, setName] = React.useState('')
  const [bio, setBio] = React.useState('')
  const [age, setAge] = React.useState(18)

  return (
    <div>
      {/* Single-line text */}
      <label>Name:</label>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Please enter your name"
      />

      {/* Numbers */}
      <label>Age:</label>
      <input
        type="number"
        value={age}
        onChange={e => setAge(Number(e.target.value))}  // Convert to a number!
        min={0} max={150}
      />

      {/* Multi-line text */}
      <label>Personal Profile:</label>
      <textarea
        value={bio}
        onChange={e => setBio(e.target.value)}
        rows={4}
        placeholder="Introduce Yourself..."
      />
    </div>
  )
}
▶ Try it Yourself

Note: In React, use the <textarea> property instead of the child text content. The e.target.value value for <input type="number"> is a string; be sure to convert it to a number.

(2) Checkbox

JSX
function SelectInputs() {
  const [city, setCity] = React.useState('')
  const [hobbies, setHobbies] = React.useState([])

  return (
    <div>
      {/* Single-selection drop-down menu */}
      <label>City:</label>
      <select value={city} onChange={e => setCity(e.target.value)}>
        <option value="">-- Please select --</option>
        <option value="beijing">Beijing</option>
        <option value="shanghai">Shanghai</option>
        <option value="shenzhen">Shenzhen</option>
      </select>

      {/* Multiple-choice drop-down list */}
      <label>Hobbies:</label>
      <select
        multiple
        value={hobbies}
        onChange={e => {
          const selected = Array.from(e.target.options)
            .filter(o => o.selected)
            .map(o => o.value)
          setHobbies(selected)
        }}
      >
        <option value="coding">Programming</option>
        <option value="reading">Read</option>
        <option value="gaming">Games</option>
      </select>
    </div>
  )
}
▶ Try it Yourself

(3) Checkboxes and Radio Buttons

JSX
function CheckboxAndRadio() {
  const [agree, setAgree] = React.useState(false)
  const [gender, setGender] = React.useState('')
  const [interests, setInterests] = React.useState([])

  // Multiple Choice:Toggle Inclusion/Exclusion
  function toggleInterest(value) {
    setInterests(prev =>
      prev.includes(value)
        ? prev.filter(i => i !== value)
        : [...prev, value]
    )
  }

  return (
    <div>
      {/* Single checkbox */}
      <label>
        <input
          type="checkbox"
          checked={agree}
          onChange={e => setAgree(e.target.checked)}  // Please note that checked No value
        />
        Agree to the User Agreement
      </label>
      {agree && <p style={{ color: '#52c41a' }}>✅ Agreed</p>}

      {/* Radio Button Group:The same one name */}
      <p>Gender:</p>
      {['Male', 'Female', 'Other'].map(g => (
        <label key={g} style={{ marginRight: '12px' }}>
          <input
            type="radio"
            name="gender"
            value={g}
            checked={gender === g}
            onChange={e => setGender(e.target.value)}
          />
          {g}
        </label>
      ))}

      {/* Multiple-Selection Checkbox Group */}
      <p>Areas of Interest:</p>
      {['Front End', 'Backend', 'AI', 'Design'].map(item => (
        <label key={item} style={{ marginRight: '12px' }}>
          <input
            type="checkbox"
            checked={interests.includes(item)}
            onChange={() => toggleInterest(item)}
          />
          {item}
        </label>
      ))}
    </div>
  )
}

▶ Example 1: Quick Reference Table for Form Element Types

Output:

TEXT 📖 Display only
Form with input fields and submit handling. TypeScript-typed React component with interface props
JSX
// ============================================
// Example:Controlled Syntax for All Common Form Elements
// ============================================

function FormElementReference() {
  const [text, setText] = React.useState('')
  const [number, setNumber] = React.useState(0)
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [bio, setBio] = React.useState('')
  const [city, setCity] = React.useState('')
  const [agree, setAgree] = React.useState(false)
  const [gender, setGender] = React.useState('')
  const [file, setFile] = React.useState(null)

  return (
    <div>
      {/* Text Input */}
      <input type="text" value={text} onChange={e => setText(e.target.value)} />
      
      {/* Numeric Input */}
      <input type="number" value={number} onChange={e => setNumber(Number(e.target.value))} />
      
      {/* Enter your email address */}
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      
      {/* Password Entry */}
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      
      {/* Multi-line text */}
      <textarea value={bio} onChange={e => setBio(e.target.value)} />
      
      {/* Drop-down menu */}
      <select value={city} onChange={e => setCity(e.target.value)}>
        <option value="">Please select</option>
        <option value="beijing">Beijing</option>
      </select>
      
      {/* Single checkbox */}
      <input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
      
      {/* Radio button */}
      <input type="radio" name="gender" value="male" checked={gender === 'male'}
        onChange={e => setGender(e.target.value)} />
      
      {/* File Upload(Uncontrolled) */}
      <input type="file" onChange={e => setFile(e.target.files[0])} />
      {/* Note:File input must be in uncontrolled mode. */}
    </div>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Reference: text (value/onChange), number (Number() cast), email, password, textarea (value), select (value), checkbox (checked), radio (checked), file (uncontrolled, files[0])


5. Form Validation

Verification Timing Triggering Event Advantages Disadvantages Recommended Scenarios
During input onChange Real-time feedback; users can correct errors immediately Triggered frequently; an error is reported before the user finishes typing Password strength, username validation
When out of focus onBlur Minimize distractions; validate only after completion Users see errors only after leaving the field Most form fields
Upon submission onSubmit Final review and unified validation User discovers an error only after filling out the form Final fallback validation

(1) Real-time Verification

JSX
function ValidatedForm() {
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [errors, setErrors] = React.useState({})
  const [touched, setTouched] = React.useState({})  // Record which fields have been accessed

  function validate(fieldName, value) {
    const newErrors = { ...errors }
    
    if (fieldName === 'email') {
      if (!value) newErrors.email = 'The email address cannot be left blank.'
      else if (!value.includes('@')) newErrors.email = 'The email address format is incorrect.'
      else delete newErrors.email
    }
    
    if (fieldName === 'password') {
      if (!value) newErrors.password = 'The password cannot be empty.'
      else if (value.length < 6) newErrors.password = 'Password must be at least 6 chars'
      else if (!/[A-Z]/.test(value)) newErrors.password = 'Passwords must contain uppercase letters'
      else delete newErrors.password
    }

    setErrors(newErrors)
  }

  function handleBlur(e) {
    const { name, value } = e.target
    setTouched({ ...touched, [name]: true })
    validate(name, value)
  }

  function handleSubmit(e) {
    e.preventDefault()
    // Validate all fields before submitting
    const allErrors = {}
    if (!email || !email.includes('@')) allErrors.email = 'Please enter a valid email address'
    if (!password || password.length < 6) allErrors.password = 'Password must be at least 6 chars'
    
    if (Object.keys(allErrors).length > 0) {
      setErrors(allErrors)
      setTouched({ email: true, password: true })
      return
    }

    alert('Submission Successful!')
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
      <div style={{ marginBottom: '16px' }}>
        <label>Email:</label>
        <input
          name="email"
          type="email"
          value={email}
          onChange={e => { setEmail(e.target.value); validate('email', e.target.value) }}
          onBlur={handleBlur}
          style={inputStyle(touched.email && errors.email)}
        />
        {touched.email && errors.email && (
          <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
        )}
      </div>

      <div style={{ marginBottom: '16px' }}>
        <label>Password:</label>
        <input
          name="password"
          type="password"
          value={password}
          onChange={e => { setPassword(e.target.value); validate('password', e.target.value) }}
          onBlur={handleBlur}
          style={inputStyle(touched.password && errors.password)}
        />
        {touched.password && errors.password && (
          <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
        )}
      </div>

      {/* Password Strength Indicator */}
      {password && (
        <div style={{ marginBottom: '16px' }}>
          <p style={{ fontSize: '12px', color: '#666' }}>
            Password Strength:
            <span style={{ color: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a' }}>
              {password.length < 6 ? 'Weak' : password.length < 10 ? 'Medium' : 'Strong'}
            </span>
          </p>
          <div style={{ height: '4px', backgroundColor: '#f0f0f0', borderRadius: '2px' }}>
            <div style={{
              height: '100%',
              width: `${Math.min(100, (password.length / 12) * 100)}%`,
              backgroundColor: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a',
              borderRadius: '2px',
              transition: 'width 0.3s'
            }} />
          </div>
        </div>
      )}

      <button type="submit" disabled={Object.keys(errors).length > 0}
        style={{
          width: '100%', padding: '10px',
          backgroundColor: Object.keys(errors).length > 0 ? '#d9d9d9' : '#1890ff',
          color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
        }}>
        Register
      </button>
    </form>
  )
}

function inputStyle(hasError) {
  return {
    width: '100%', padding: '8px',
    border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
    borderRadius: '4px',
    outline: 'none'
  }
}

(2) Customize the useForm hook

Extract the form logic into a custom hook for easy reuse:

JSX
// ============================================
// Example:Custom useForm Hook
// Features:Unified Management of Form Status、Verification、Submit
// ============================================

function useForm(initialValues, validateFn) {
  const [values, setValues] = React.useState(initialValues)
  const [errors, setErrors] = React.useState({})
  const [touched, setTouched] = React.useState({})

  // Handling Input Changes
  function handleChange(e) {
    const { name, value, type, checked } = e.target
    const newValue = type === 'checkbox' ? checked : value
    const newValues = { ...values, [name]: newValue }
    setValues(newValues)
    
    // Real-Time Verification
    if (validateFn) {
      const newErrors = validateFn(newValues)
      setErrors(newErrors)
    }
  }

  // Handling Loss of Focus
  function handleBlur(e) {
    const { name } = e.target
    setTouched({ ...touched, [name]: true })
  }

  // Process Submission
  function handleSubmit(callback) {
    return (e) => {
      e.preventDefault()
      // Mark all fields as checked
      const allTouched = Object.keys(values).reduce((acc, key) => {
        acc[key] = true
        return acc
      }, {})
      setTouched(allTouched)
      
      // Verification
      if (validateFn) {
        const newErrors = validateFn(values)
        setErrors(newErrors)
        if (Object.keys(newErrors).length > 0) return
      }
      
      callback(values)
    }
  }

  // Reset Form
  function reset() {
    setValues(initialValues)
    setErrors({})
    setTouched({})
  }

  return { values, errors, touched, handleChange, handleBlur, handleSubmit, reset }
}

// ---- Usage useForm ----
function LoginForm() {
  function validate(values) {
    const errors = {}
    if (!values.email) errors.email = 'Please enter your email address'
    else if (!values.email.includes('@')) errors.email = 'The email address format is incorrect.'
    if (!values.password) errors.password = 'Please enter your password'
    else if (values.password.length < 6) errors.password = 'Password must be at least 6 chars'
    return errors
  }

  const form = useForm({ email: '', password: '' }, validate)

  return (
    <form onSubmit={form.handleSubmit(data => console.log('Submit:', data))}
      style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h2>Log In</h2>
      
      <input
        name="email"
        value={form.values.email}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
        placeholder="Email"
        style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.email && form.errors.email ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
      />
      {form.touched.email && form.errors.email && (
        <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.email}</p>
      )}

      <input
        name="password"
        type="password"
        value={form.values.password}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
        placeholder="Password"
        style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.password && form.errors.password ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
      />
      {form.touched.password && form.errors.password && (
        <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.password}</p>
      )}

      <button type="submit" style={{
        width: '100%', padding: '10px', backgroundColor: '#1890ff',
        color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
      }}>
        Log In
      </button>
      <button type="button" onClick={form.reset}
        style={{ width: '100%', padding: '8px', marginTop: '8px', cursor: 'pointer' }}>
        Reset
      </button>
    </form>
  )
}


6. Complete Example: User Registration Form

JSX
// ============================================
// Complete Example:User Registration Form
// Features:Controlled Components + Real-Time Verification + Form Data Synchronization
// ============================================

function RegistrationForm() {
  const [form, setForm] = React.useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    country: '',
    gender: '',
    acceptTerms: false
  })
  const [errors, setErrors] = React.useState({})

  function validate() {
    const newErrors = {}
    if (!form.username || form.username.length < 2)
      newErrors.username = 'Username must be at least 2 characters'
    if (!form.email || !form.email.includes('@'))
      newErrors.email = 'Please enter a valid email address'
    if (!form.password || form.password.length < 6)
      newErrors.password = 'Password must be at least 6 chars'
    if (form.password !== form.confirmPassword)
      newErrors.confirmPassword = 'The two passwords do not match'
    if (!form.country)
      newErrors.country = 'Please select a country'
    if (!form.acceptTerms)
      newErrors.acceptTerms = 'Please agree to the Terms of Service'
    return newErrors
  }

  function handleChange(e) {
    const { name, value, type, checked } = e.target
    const newValue = type === 'checkbox' ? checked : value
    const newForm = { ...form, [name]: newValue }
    setForm(newForm)
    setErrors(validate())
    // Interconnection:Automatically adjust password rule prompts after selecting a country
  }

  function handleSubmit(e) {
    e.preventDefault()
    const finalErrors = validate()
    setErrors(finalErrors)
    if (Object.keys(finalErrors).length === 0) {
      alert(`Registration Successful!Welcome ${form.username}`)
    }
  }

  function inputStyle(fieldName) {
    return {
      width: '100%', padding: '8px', marginBottom: '4px',
      border: `1px solid ${errors[fieldName] ? '#ff4d4f' : '#d9d9d9'}`,
      borderRadius: '4px'
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📝 User Registration</h2>

      <input name="username" value={form.username} onChange={handleChange}
        placeholder="Username" style={inputStyle('username')} />
      {errors.username && <p style={errorStyle}>{errors.username}</p>}

      <input name="email" type="email" value={form.email} onChange={handleChange}
        placeholder="Email" style={inputStyle('email')} />
      {errors.email && <p style={errorStyle}>{errors.email}</p>}

      <input name="password" type="password" value={form.password} onChange={handleChange}
        placeholder="Password (at least 6 chars)" style={inputStyle('password')} />
      {errors.password && <p style={errorStyle}>{errors.password}</p>}

      <input name="confirmPassword" type="password" value={form.confirmPassword}
        onChange={handleChange} placeholder="Confirm Password" style={inputStyle('confirmPassword')} />
      {errors.confirmPassword && <p style={errorStyle}>{errors.confirmPassword}</p>}

      <select name="country" value={form.country} onChange={handleChange}
        style={inputStyle('country')}>
        <option value="">-- Select a Country --</option>
        <option value="CN">China</option>
        <option value="US">United States</option>
        <option value="JP">Japan</option>
        <option value="BR">Brazil</option>
        <option value="SA">Saudi Arabia</option>
      </select>
      {errors.country && <p style={errorStyle}>{errors.country}</p>}

      <div style={{ margin: '12px 0' }}>
        <p>Gender:</p>
        {['Male', 'Female', 'Other'].map(g => (
          <label key={g} style={{ marginRight: '16px' }}>
            <input type="radio" name="gender" value={g}
              checked={form.gender === g} onChange={handleChange} />
            {g}
          </label>
        ))}
      </div>

      <label style={{ display: 'block', margin: '12px 0' }}>
        <input type="checkbox" name="acceptTerms" checked={form.acceptTerms}
          onChange={handleChange} />
        I have read and agree to <a href="#">User Agreement</a>
      </label>
      {errors.acceptTerms && <p style={errorStyle}>{errors.acceptTerms}</p>}

      <button type="submit" style={{
        width: '100%', padding: '10px',
        backgroundColor: Object.keys(errors).length === 0 && form.username ? '#1890ff' : '#d9d9d9',
        color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
      }}>
        Register
      </button>
    </form>
  )
}

const errorStyle = {
  color: '#ff4d4f', fontSize: '12px',
  margin: '0 0 8px 0'
}

▶ Example 2: Form Data Binding—Automatically Filling in Area Codes When a Country Is Selected

Output:

TEXT 📖 Display only
Displays: "Please select". Input types: text, email, number. Dropdown: Please select, Beijing
JSX
function PhoneForm() {
  const [form, setForm] = useState({ country: 'US', phone: '' })
  const areaCodes = { US: '+1', CN: '+86', JP: '+81', UK: '+44', BR: '+55' }

  function handleCountryChange(e) {
    const country = e.target.value
    setForm(prev => ({
      ...prev,
      country,
      phone: prev.phone.replace(/^\+\d+\s*/, '') + ' ',
    }))
  }

  function handleChange(e) {
    const { name, value } = e.target
    setForm(prev => ({ ...prev, [name]: value }))
  }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Phone Contact</h3>
      <select name="country" value={form.country} onChange={handleCountryChange}
        style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
        {Object.keys(areaCodes).map(c => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>
      <div style={{ display: 'flex', gap: 8 }}>
        <input value={areaCodes[form.country]} readOnly style={{ width: 60, padding: 8, borderRadius: 4 }} />
        <input name="phone" value={form.phone} onChange={handleChange}
          placeholder="Phone number" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
      </div>
      <p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
        Full: {areaCodes[form.country]} {form.phone.trim()}
      </p>
    </div>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
Country selector (US/CN/JP/UK/BR) → area code auto-fills (US: +1, CN: +86). Phone input + readonly area code. Shows "Full: +1 5551234"

▶ Example 3: Dynamic Form—Add/Remove Team Members

Output:

TEXT 📖 Display only
Displays: "Phone Contact". State: form (setter: setForm). Input: Phone number. Dropdown: {c}
JSX
function TeamForm() {
  const [members, setMembers] = useState([{ name: '', email: '', role: 'developer' }])

  function addMember() {
    setMembers(prev => [...prev, { name: '', email: '', role: 'developer' }])
  }

  function removeMember(index) {
    setMembers(prev => prev.filter((_, i) => i !== index))
  }

  function updateMember(index, field, value) {
    setMembers(prev => prev.map((m, i) => i === index ? { ...m, [field]: value } : m))
  }

  function handleSubmit(e) {
    e.preventDefault()
    console.log('Team:', members)
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '0 auto' }}>
      <h3>Team Members</h3>
      {members.map((m, i) => (
        <div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
          <input value={m.name} onChange={e => updateMember(i, 'name', e.target.value)}
            placeholder="Name" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
          <input value={m.email} onChange={e => updateMember(i, 'email', e.target.value)}
            placeholder="Email" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
          <select value={m.role} onChange={e => updateMember(i, 'role', e.target.value)}
            style={{ padding: 6, borderRadius: 4 }}>
            <option value="developer">Dev</option>
            <option value="designer">Design</option>
            <option value="manager">PM</option>
          </select>
          {members.length > 1 && (
            <button type="button" onClick={() => removeMember(i)}
              style={{ padding: '6px 10px', color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>
              X
            </button>
          )}
        </div>
      ))}
      <button type="button" onClick={addMember}
        style={{ padding: '6px 16px', marginRight: 8, cursor: 'pointer' }}>
        + Add Member
      </button>
      <button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Submit</button>
    </form>
  )
}

Output:

TEXT 📖 Display only
Dynamic member rows (Name + Email + Role dropdown). "+ Add Member" adds row, "X" removes (min 1 kept). Submit → console.log("Team:", members)

▶ Example 4: Multi-Step Form Wizard

Output:

TEXT 📖 Display only
... ('Team:', members)
JSX
function MultiStepForm() {
  const [step, setStep] = useState(1)
  const [form, setForm] = useState({ name: '', email: '', password: '', city: '', agree: false })
  const [errors, setErrors] = useState({})

  function updateField(e) {
    const { name, value, type, checked } = e.target
    setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
  }

  function validate() {
    const errs = {}
    if (step === 1) {
      if (!form.name.trim()) errs.name = 'Name required'
      if (!form.email.includes('@')) errs.email = 'Valid email required'
    } else if (step === 2) {
      if (form.password.length < 6) errs.password = 'At least 6 characters'
    } else if (step === 3) {
      if (!form.city) errs.city = 'Select a city'
      if (!form.agree) errs.agree = 'Must agree to terms'
    }
    setErrors(errs)
    return Object.keys(errs).length === 0
  }

  function next() { if (validate()) setStep(s => Math.min(s + 1, 3)) }
  function back() { setStep(s => Math.max(s - 1, 1)) }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Step {step} of 3</h3>
      <div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
        {[1, 2, 3].map(s => (
          <div key={s} style={{ flex: 1, height: 4, borderRadius: 2,
            backgroundColor: s <= step ? '#1890ff' : '#f0f0f0' }} />
        ))}
      </div>
      {step === 1 && (
        <>
          <input name="name" value={form.name} onChange={updateField} placeholder="Name"
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.name ? '1px solid #ff4d4f' : undefined }} />
          {errors.name && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.name}</p>}
          <input name="email" value={form.email} onChange={updateField} placeholder="Email"
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.email ? '1px solid #ff4d4f' : undefined }} />
          {errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.email}</p>}
        </>
      )}
      {step === 2 && (
        <>
          <input name="password" type="password" value={form.password} onChange={updateField}
            placeholder="Password" style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }} />
          {errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.password}</p>}
        </>
      )}
      {step === 3 && (
        <>
          <select name="city" value={form.city} onChange={updateField}
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }}>
            <option value="">Select city</option>
            <option value="beijing">Beijing</option>
            <option value="shanghai">Shanghai</option>
          </select>
          {errors.city && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.city}</p>}
          <label style={{ display: 'block', marginTop: 8 }}>
            <input type="checkbox" name="agree" checked={form.agree} onChange={updateField} />
            I agree to the terms
          </label>
          {errors.agree && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.agree}</p>}
        </>
      )}
      <div style={{ marginTop: 16 }}>
        {step > 1 && <button type="button" onClick={back} style={{ marginRight: 8, cursor: 'pointer' }}>Back</button>}
        {step < 3 ? (
          <button type="button" onClick={next} style={{ cursor: 'pointer' }}>Next</button>
        ) : (
          <button type="button" onClick={() => { if (validate()) alert('Done!') }} style={{ cursor: 'pointer' }}>Submit</button>
        )}
      </div>
    </div>
  )
}

Output:

TEXT 📖 Display only
3-step wizard with progress bar. Step 1: Name+Email, Step 2: Password (6+ chars), Step 3: City+Terms. Next validates, Back returns. Submit → "Done!"

▶ Example 5: Unmanaged Components—File Upload and useRef

Output:

TEXT 📖 Display only
Displays "file upload (uncontrolled)". state: preview, filename. buttons: upload. uses useref
JSX
function FileUploadForm() {
  const fileInputRef = useRef(null)
  const [preview, setPreview] = useState(null)
  const [fileName, setFileName] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    const file = fileInputRef.current?.files[0]
    if (!file) { alert('Please select a file'); return }
    console.log('Uploading:', file.name, file.size, 'bytes')
    alert(`File "${file.name}" (${(file.size / 1024).toFixed(1)} KB) selected for upload`)
  }

  function handleFileChange(e) {
    const file = e.target.files[0]
    if (!file) { setPreview(null); setFileName(''); return }
    setFileName(file.name)
    if (file.type.startsWith('image/')) {
      const reader = new FileReader()
      reader.onload = (e) => setPreview(e.target.result)
      reader.readAsDataURL(file)
    } else {
      setPreview(null)
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>File Upload (Uncontrolled)</h3>
      <div style={{ marginBottom: 12 }}>
        <input type="file" ref={fileInputRef} onChange={handleFileChange}
          accept="image/*,.pdf,.doc" style={{ marginBottom: 8 }} />
        {fileName && <p style={{ fontSize: 13, color: '#666' }}>Selected: {fileName}</p>}
      </div>
      {preview && (
        <div style={{ marginBottom: 12 }}>
          <img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: 200, borderRadius: 4, border: '1px solid #eee' }} />
        </div>
      )}
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Upload</button>
    </form>
  )
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
File input → image preview shown, or filename displayed. Upload alerts "File "photo.jpg" (245.3 KB) selected for upload"

❓ FAQ

Q For input[type="checkbox"], should I use checked or value?
A For checkboxes, use the checked attribute (boolean), not value. To retrieve the new value in the corresponding onChange event, use e.target.checked. Similarly, for radio buttons, use checked.
Q When should form validation be performed?
A Each of the three timing options serves a different purpose: ① Validation as the user types (onChange) — provides real-time feedback but triggers frequently; ② Validation when the field loses focus (onBlur) — minimizes disruption by displaying errors only after the user has finished filling out the form; ③ Validation upon submission (onSubmit) — serves as the final check. We recommend using a combination of onBlur and onSubmit, with onChange added to critical fields such as the username for real-time feedback.
Q What is the general approach for handling handleChange when a form has many fields?
A Use e.target.name in combination with the computed property name: const { name, value } = e.target; setForm(prev => ({...prev, [name]: value})). This way, all <input name="xxx"> can use the same handleChange. Checkboxes require special handling: const newValue = type === 'checkbox' ? checked : value.
Q How do you decide between controlled and uncontrolled components?
A A simple rule: If real-time validation, formatting, or conditional display is required → use a controlled component (e.g., real-time password strength prompts, automatic amount formatting). If you’re only concerned with the final value or reading the value only upon submission → use an uncontrolled component (e.g., file uploads, simple contact forms). <input type="file"> You can only use uncontrolled components because their value is read-only. useForm Hooks can encapsulate boilerplate code for controlled components.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Create a FeedbackForm component that includes name, email, rating (dropdown options: Positive/Neutral/Negative), and content (textarea). When submitted, display all data in an alert.
  2. Advanced Problem (Difficulty ⭐⭐): Create a PasswordStrengthMeter component that displays password strength (weak/medium/strong) in real time. Rules: ≥8 characters + uppercase and lowercase letters + numbers = strong; ≥6 characters = medium; otherwise = weak.
  3. Challenge (Difficulty: ⭐⭐⭐): Create a DynamicForm component that allows users to dynamically add or remove form items (e.g., "Add multiple family members"). Each form item should include three fields: name, age, and relationship. Upon submission, the component should output an array.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏