【React-8/Lesson91(2025-12-29)】React 受控组件与非受控组件完全指南🎯

7 阅读7分钟

📖 前言

在 React 开发中,表单处理是日常开发中最常见的场景之一。当我们需要获取用户输入的数据时,React 提供了两种主要的方式:受控组件和非受控组件。理解这两种方式的区别、使用场景以及各自的优缺点,对于编写高质量的 React 应用至关重要。

🤔 怎么拿到表单的值?

在传统的 HTML 开发中,我们通常通过 DOM 操作直接获取表单元素的值。但在 React 中,由于采用了虚拟 DOM 和单向数据流的设计理念,获取表单值的方式发生了根本性的变化。

📝 表单元素的基本特性

表单元素具有一个特殊的属性:value。这个属性是双向的,它既需要被设置,又需要能够通过用户输入来改变。在 React 中,处理这种双向性主要有两种方式:

  1. 受控组件:通过 React 状态来控制表单元素的值
  2. 非受控组件:通过 DOM 引用直接获取表单元素的值

✅ 受控组件详解

🎨 核心概念

受控组件是指表单元素的值完全由 React 状态控制的组件。在这种模式下,表单元素的值与 React 状态保持同步,形成单向数据绑定。

💡 工作原理

const [r, setValue] = useState('')

<input 
  type="text" 
  value={r} 
  onChange={(e) => setValue(e.target.value)}
/>

这个简单的例子展示了受控组件的核心机制:

  1. 状态驱动页面:输入框的值由 value 状态决定
  2. 单向数据绑定:状态的变化会自动反映到 UI 上
  3. 用户输入收集:通过 onChange 事件监听用户输入
  4. 状态更新:调用 setValue 更新状态,触发组件重新渲染

🔄 完整的受控表单示例

下面是一个完整的登录表单示例,展示了如何使用受控组件处理多个表单字段:

import { useState } from 'react'

export default function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: ''
  })

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

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        placeholder="请输入用户名"
        name="username"
        onChange={handleChange}
        value={form.username}
      />
      <input 
        type="password"
        placeholder="请输入密码"
        name="password"
        onChange={handleChange}
        value={form.password}
      />
      <button type="submit">注册</button>
    </form>
  )
}

🎯 受控组件的优势

  1. 数据状态驱动页面:状态是唯一的真理来源,UI 只是状态的反映
  2. 实时验证:可以在用户输入时立即进行表单验证
  3. 字段联动:一个字段的值可以影响其他字段的显示或禁用状态
  4. 可预测性:状态的变化是可追踪和可预测的
  5. 易于测试:可以通过状态来测试组件的行为

📊 受控组件的使用场景

  • 需要实时验证用户输入的场景
  • 表单字段之间存在联动关系
  • 需要在提交前进行复杂的表单处理
  • 需要实现表单的自动保存功能
  • 需要对表单值进行格式化处理

🔓 非受控组件详解

🎨 核心概念

非受控组件是指表单元素的值不由 React 状态控制,而是通过 DOM 引用直接获取的组件。在这种模式下,表单元素的行为类似于传统的 HTML 表单。

💡 工作原理

import { useRef } from 'react'

const inputRef = useRef(null)

<input type="text" ref={inputRef} />

const getValue = () => {
  console.log(inputRef.current.value)
}

非受控组件的核心机制:

  1. 使用 useRef 创建引用useRef 返回一个可变的 ref 对象
  2. 绑定到表单元素:通过 ref 属性将引用绑定到 DOM 元素
  3. 直接访问 DOM:通过 ref.current.value 直接获取表单值
  4. 不触发重新渲染:获取值不会触发组件重新渲染

🔄 完整的非受控表单示例

下面是一个评论框组件的示例,展示了非受控组件的使用:

import { useRef } from 'react'

export default function CommentBox() {
  const textareaRef = useRef(null)

  const handleSubmit = () => {
    const comment = textareaRef.current.value
    if (!comment) return alert('请输入评论')
    console.log(comment)
  }

  return (
    <div>
      <textarea 
        ref={textareaRef}
        placeholder="输入评论..."
      />
      <button onClick={handleSubmit}>提交</button>
    </div>
  )
}

🎯 非受控组件的优势

  1. 性能更好:不需要每次输入都触发状态更新和重新渲染
  2. 代码更简洁:不需要为每个表单字段维护状态
  3. 适合一次性读取:只在需要时获取表单值
  4. 兼容传统代码:更容易迁移传统的 HTML 表单代码

📊 非受控组件的使用场景

  • 一次性读取表单值的场景
  • 对性能敏感的应用
  • 文件上传等特殊表单元素
  • 简单的表单,不需要实时验证
  • 需要集成第三方表单库

⚖️ 受控组件与非受控组件的区别

📋 对比表格

特性受控组件非受控组件
数据来源React 状态DOM 元素
更新机制状态更新触发重新渲染直接操作 DOM
性能每次输入触发重新渲染不触发重新渲染
实时验证容易实现需要手动处理
代码复杂度需要维护状态代码简洁
适用场景复杂表单、需要验证简单表单、性能敏感

🎯 使用场景对比

受控组件适合的场景:

  • 表单校验:需要在用户输入时实时验证数据格式
  • 字段联动:一个字段的值影响其他字段的显示或禁用状态
  • 实时展示:需要实时显示用户输入的内容或格式化结果
  • 表单的及时验证:在提交前进行复杂的表单验证
  • 数据绑定:需要将表单值与其他组件或状态绑定

非受控组件适合的场景:

  • 一次性读取:只在提交时读取表单值
  • 性能敏感:对性能要求高,避免频繁重新渲染
  • 文件上传:处理文件上传等特殊表单元素
  • 简单表单:表单逻辑简单,不需要实时验证
  • 第三方集成:需要集成第三方表单库或组件

🔧 高级技巧和最佳实践

📦 受控组件的优化

1. 使用单一状态对象

const [form, setForm] = useState({
  username: '',
  password: '',
  email: ''
})

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

2. 使用自定义 Hook 封装表单逻辑

function useForm(initialValues) {
  const [values, setValues] = useState(initialValues)

  const handleChange = (e) => {
    setValues({
      ...values,
      [e.target.name]: e.target.value
    })
  }

  const resetForm = () => {
    setValues(initialValues)
  }

  return { values, handleChange, resetForm }
}

3. 实现防抖优化

import { useState, useEffect } from 'react'
import { debounce } from 'lodash'

const [value, setValue] = useState('')
const [debouncedValue, setDebouncedValue] = useState('')

const handleChange = (e) => {
  setValue(e.target.value)
}

useEffect(() => {
  const handler = debounce(() => {
    setDebouncedValue(value)
  }, 300)

  handler()

  return () => {
    handler.cancel()
  }
}, [value])

📦 非受控组件的优化

1. 使用 getDefaultValue 设置初始值

<input 
  type="text" 
  ref={inputRef} 
  defaultValue="初始值"
/>

2. 结合受控和非受控组件

import { useState, useRef } from 'react'

export default function HybridForm() {
  const [username, setUsername] = useState('')
  const passwordRef = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log({
      username,
      password: passwordRef.current.value
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input 
        type="password"
        ref={passwordRef}
      />
      <button type="submit">提交</button>
    </form>
  )
}

🎨 实际应用示例

📝 评论组件(非受控组件)

import { useRef } from 'react'

export default function CommentBox() {
  const textareaRef = useRef(null)

  const handleSubmit = () => {
    const comment = textareaRef.current.value
    if (!comment) return alert('请输入评论')
    console.log('提交评论:', comment)
    textareaRef.current.value = ''
  }

  return (
    <div>
      <textarea 
        ref={textareaRef}
        placeholder="输入评论..."
        rows={4}
        cols={50}
      />
      <button onClick={handleSubmit}>提交</button>
    </div>
  )
}

🔐 登录表单(受控组件)

import { useState } from 'react'

export default function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: ''
  })

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

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('登录信息:', form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        placeholder="请输入用户名"
        name="username"
        onChange={handleChange}
        value={form.username}
      />
      <input 
        type="password"
        placeholder="请输入密码"
        name="password"
        onChange={handleChange}
        value={form.password}
      />
      <button type="submit">注册</button>
    </form>
  )
}

🎯 混合使用示例

import { useState, useRef } from 'react'

export default function App() {
  const [value, setValue] = useState('')
  const inputRef = useRef(null)

  const doLogin = (e) => {
    e.preventDefault()
    console.log('受控组件值:', value)
    console.log('非受控组件值:', inputRef.current.value)
  }

  return (
    <form onSubmit={doLogin}>
      <input 
        type="text" 
        value={value} 
        onChange={(e) => setValue(e.target.value)}
        placeholder="受控输入框"
      />
      <input 
        type="text" 
        ref={inputRef}
        placeholder="非受控输入框"
      />
      <button type="submit">登录</button>
    </form>
  )
}

🚀 总结

React 中的受控组件和非受控组件各有其适用的场景:

  • 受控组件:通过状态控制表单元素的值,实现单向数据流,适合需要实时验证、字段联动、复杂表单处理的场景
  • 非受控组件:通过 DOM 引用直接获取表单值,性能更好,代码更简洁,适合一次性读取、性能敏感、文件上传等场景

在实际开发中,我们应该根据具体的需求和场景选择合适的方式,甚至可以在同一个应用中混合使用两种方式,以达到最佳的开发体验和性能表现。

理解这两种表单处理方式的本质区别,能够帮助我们更好地设计 React 应用的数据流架构,编写出更加高效、可维护的代码。