📖 前言
在 React 开发中,表单处理是日常开发中最常见的场景之一。当我们需要获取用户输入的数据时,React 提供了两种主要的方式:受控组件和非受控组件。理解这两种方式的区别、使用场景以及各自的优缺点,对于编写高质量的 React 应用至关重要。
🤔 怎么拿到表单的值?
在传统的 HTML 开发中,我们通常通过 DOM 操作直接获取表单元素的值。但在 React 中,由于采用了虚拟 DOM 和单向数据流的设计理念,获取表单值的方式发生了根本性的变化。
📝 表单元素的基本特性
表单元素具有一个特殊的属性:value。这个属性是双向的,它既需要被设置,又需要能够通过用户输入来改变。在 React 中,处理这种双向性主要有两种方式:
- 受控组件:通过 React 状态来控制表单元素的值
- 非受控组件:通过 DOM 引用直接获取表单元素的值
✅ 受控组件详解
🎨 核心概念
受控组件是指表单元素的值完全由 React 状态控制的组件。在这种模式下,表单元素的值与 React 状态保持同步,形成单向数据绑定。
💡 工作原理
const [r, setValue] = useState('')
<input
type="text"
value={r}
onChange={(e) => setValue(e.target.value)}
/>
这个简单的例子展示了受控组件的核心机制:
- 状态驱动页面:输入框的值由
value状态决定 - 单向数据绑定:状态的变化会自动反映到 UI 上
- 用户输入收集:通过
onChange事件监听用户输入 - 状态更新:调用
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>
)
}
🎯 受控组件的优势
- 数据状态驱动页面:状态是唯一的真理来源,UI 只是状态的反映
- 实时验证:可以在用户输入时立即进行表单验证
- 字段联动:一个字段的值可以影响其他字段的显示或禁用状态
- 可预测性:状态的变化是可追踪和可预测的
- 易于测试:可以通过状态来测试组件的行为
📊 受控组件的使用场景
- 需要实时验证用户输入的场景
- 表单字段之间存在联动关系
- 需要在提交前进行复杂的表单处理
- 需要实现表单的自动保存功能
- 需要对表单值进行格式化处理
🔓 非受控组件详解
🎨 核心概念
非受控组件是指表单元素的值不由 React 状态控制,而是通过 DOM 引用直接获取的组件。在这种模式下,表单元素的行为类似于传统的 HTML 表单。
💡 工作原理
import { useRef } from 'react'
const inputRef = useRef(null)
<input type="text" ref={inputRef} />
const getValue = () => {
console.log(inputRef.current.value)
}
非受控组件的核心机制:
- 使用 useRef 创建引用:
useRef返回一个可变的 ref 对象 - 绑定到表单元素:通过
ref属性将引用绑定到 DOM 元素 - 直接访问 DOM:通过
ref.current.value直接获取表单值 - 不触发重新渲染:获取值不会触发组件重新渲染
🔄 完整的非受控表单示例
下面是一个评论框组件的示例,展示了非受控组件的使用:
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>
)
}
🎯 非受控组件的优势
- 性能更好:不需要每次输入都触发状态更新和重新渲染
- 代码更简洁:不需要为每个表单字段维护状态
- 适合一次性读取:只在需要时获取表单值
- 兼容传统代码:更容易迁移传统的 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 应用的数据流架构,编写出更加高效、可维护的代码。