React: Forms and Controlled Components

Last updated: 2026-10-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()
    // On submit, read the form field values
    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

▶ 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

▶ 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

▶ Try it Yourself

(3) Checkboxes and Radio Buttons

▶ Try it Yourself

Output:

TEXT
Form with input fields and submit handling. TypeScript-typed React component with interface props

▶ Example 1: Quick Reference Table for Form Element Types

▶ Try it Yourself

Output:

TEXT
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'
  }
}
▶ Try it Yourself

(2) Customize the useForm hook

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

▶ Expand runnable example (91 lines)
▶ Try it Yourself

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'
}
▶ Try it Yourself

Output:

TEXT
Displays: "Please select". Input types: text, email, number. Dropdown: Please select, Beijing

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

▶ Try it Yourself

Output:

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

Output:

TEXT
Displays: "Phone Contact". State: form (setter: setForm). Input: Phone number. Dropdown: {c}

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

▶ Try it Yourself

Output:

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

Output:

TEXT
... ('Team:', members)

▶ Example 4: Multi-Step Form Wizard

▶ Expand runnable example (77 lines)
▶ Try it Yourself

Output:

TEXT
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!"

Output:

TEXT
Displays "file upload (uncontrolled)". state: preview, filename. buttons: upload. uses useref

▶ Example 5: Unmanaged Components—File Upload and 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
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%

🙏 帮我们做得更好

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

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