React 受控组件与非受控组件详解
一、核心概念对比
1.1 受控组件 (Controlled Components)
- 数据由React state控制:表单元素的值完全由React组件的state管理
- 单向数据流:state → 表单值
- 实时同步:每次用户输入都会触发state更新和组件重新渲染
1.2 非受控组件 (Uncontrolled Components)
- 数据由DOM管理:表单元素的值由DOM自身维护
- 按需获取:通过ref在需要时访问DOM值
- 更接近原生HTML:性能更好,但集成度较低
二、受控组件详解
2.1 基本使用
jsx
import React, { useState } from 'react';
function ControlledForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交的数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用户名:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
<div>
<label>邮箱:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label>密码:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<button type="submit">提交</button>
</form>
);
}
2.2 各种表单元素的受控实现
文本框
jsx
const [text, setText] = useState('');
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
文本域
jsx
const [content, setContent] = useState('');
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
单选按钮
jsx
const [gender, setGender] = useState('male');
<div>
<label>
<input
type="radio"
value="male"
checked={gender === 'male'}
onChange={(e) => setGender(e.target.value)}
/>
男
</label>
<label>
<input
type="radio"
value="female"
checked={gender === 'female'}
onChange={(e) => setGender(e.target.value)}
/>
女
</label>
</div>
复选框
jsx
// 单个复选框
const [agreed, setAgreed] = useState(false);
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
// 多个复选框
const [hobbies, setHobbies] = useState({
reading: false,
coding: false,
gaming: false
});
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setHobbies(prev => ({
...prev,
[name]: checked
}));
};
下拉选择框
jsx
const [country, setCountry] = useState('china');
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="china">中国</option>
<option value="usa">美国</option>
<option value="japan">日本</option>
</select>
// 多选
const [selectedCities, setSelectedCities] = useState([]);
<select
multiple
value={selectedCities}
onChange={(e) => {
const options = Array.from(e.target.selectedOptions);
setSelectedCities(options.map(option => option.value));
}}
>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
2.3 表单验证
jsx
function ValidatedForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (name, value) => {
const newErrors = { ...errors };
if (name === 'email') {
if (!value) {
newErrors.email = '邮箱不能为空';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
newErrors.email = '邮箱格式不正确';
} else {
delete newErrors.email;
}
}
if (name === 'password') {
if (!value) {
newErrors.password = '密码不能为空';
} else if (value.length < 6) {
newErrors.password = '密码至少6位';
} else {
delete newErrors.password;
}
}
setErrors(newErrors);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// 实时验证
validate(name, value);
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
};
return (
<form>
<div>
<input
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && (
<span style={{ color: 'red' }}>{errors.password}</span>
)}
</div>
</form>
);
}
2.4 动态表单字段
jsx
function DynamicForm() {
const [fields, setFields] = useState([{ value: '' }]);
const addField = () => {
setFields([...fields, { value: '' }]);
};
const removeField = (index) => {
setFields(fields.filter((_, i) => i !== index));
};
const handleFieldChange = (index, value) => {
const newFields = [...fields];
newFields[index].value = value;
setFields(newFields);
};
return (
<div>
{fields.map((field, index) => (
<div key={index}>
<input
value={field.value}
onChange={(e) => handleFieldChange(index, e.target.value)}
/>
{fields.length > 1 && (
<button type="button" onClick={() => removeField(index)}>
删除
</button>
)}
</div>
))}
<button type="button" onClick={addField}>
添加字段
</button>
</div>
);
}
三、非受控组件详解
3.1 基本使用
jsx
import React, { useRef } from 'react';
function UncontrolledForm() {
const usernameRef = useRef();
const emailRef = useRef();
const fileRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
username: usernameRef.current.value,
email: emailRef.current.value,
// 文件输入特别适合使用非受控组件
file: fileRef.current.files[0]
};
console.log('表单数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用户名:</label>
<input
type="text"
ref={usernameRef}
defaultValue="默认用户名"
/>
</div>
<div>
<label>邮箱:</label>
<input
type="email"
ref={emailRef}
/>
</div>
<div>
<label>上传文件:</label>
<input
type="file"
ref={fileRef}
/>
</div>
<button type="submit">提交</button>
</form>
);
}
3.2 各种表单元素的非受控实现
使用 defaultValue
jsx
function UncontrolledInputs() {
const inputRef = useRef();
const textareaRef = useRef();
const selectRef = useRef();
return (
<div>
{/* 文本框 */}
<input
type="text"
ref={inputRef}
defaultValue="默认文本"
/>
{/* 文本域 */}
<textarea
ref={textareaRef}
defaultValue="默认内容"
/>
{/* 下拉选择 */}
<select ref={selectRef} defaultValue="option2">
<option value="option1">选项1</option>
<option value="option2">选项2</option>
</select>
{/* 复选框 */}
<input
type="checkbox"
defaultChecked
ref={useRef()}
/>
{/* 单选按钮 */}
<input
type="radio"
defaultChecked
ref={useRef()}
/>
</div>
);
}
3.3 文件上传(非受控的典型用例)
jsx
function FileUpload() {
const fileInputRef = useRef();
const handleSubmit = async (e) => {
e.preventDefault();
const file = fileInputRef.current.files[0];
if (!file) {
alert('请选择文件');
return;
}
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
// 上传文件
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
};
const handlePreview = () => {
const file = fileInputRef.current.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
// 预览图片
const img = document.createElement('img');
img.src = e.target.result;
document.body.appendChild(img);
};
reader.readAsDataURL(file);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="file"
ref={fileInputRef}
accept="image/*, .pdf, .doc"
multiple // 支持多文件上传
/>
<button type="button" onClick={handlePreview}>
预览
</button>
<button type="submit">上传</button>
</form>
);
}
3.4 第三方库集成
jsx
import React, { useEffect, useRef } from 'react';
import DatePicker from 'some-datepicker-library';
function ThirdPartyIntegration() {
const datePickerRef = useRef();
const editorRef = useRef();
useEffect(() => {
// 初始化第三方日期选择器
const datePicker = new DatePicker(datePickerRef.current, {
format: 'YYYY-MM-DD'
});
// 初始化富文本编辑器
const editor = new RichTextEditor(editorRef.current);
return () => {
// 清理
datePicker.destroy();
editor.destroy();
};
}, []);
const getValues = () => {
return {
date: datePickerRef.current.value,
content: editorRef.current.innerHTML
};
};
return (
<div>
<input type="text" ref={datePickerRef} />
<div ref={editorRef}></div>
</div>
);
}
四、混合使用场景
4.1 受控与非受控结合
jsx
function HybridForm() {
// 受控状态
const [userInfo, setUserInfo] = useState({
name: '',
age: ''
});
// 非受控引用
const fileInputRef = useRef();
const colorPickerRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
...userInfo,
file: fileInputRef.current.files[0],
color: colorPickerRef.current.value
};
console.log('完整表单数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* 受控组件 */}
<input
type="text"
value={userInfo.name}
onChange={(e) => setUserInfo({...userInfo, name: e.target.value})}
placeholder="姓名"
/>
<input
type="number"
value={userInfo.age}
onChange={(e) => setUserInfo({...userInfo, age: e.target.value})}
placeholder="年龄"
/>
{/* 非受控组件 */}
<input
type="file"
ref={fileInputRef}
/>
<input
type="color"
ref={colorPickerRef}
defaultValue="#000000"
/>
<button type="submit">提交</button>
</form>
);
}
4.2 性能优化:debounce受控输入
jsx
import React, { useState, useCallback } from 'react';
import { debounce } from 'lodash';
function DebouncedInput() {
const [value, setValue] = useState('');
const [searchResult, setSearchResult] = useState('');
// 防抖的搜索函数
const debouncedSearch = useCallback(
debounce((searchTerm) => {
console.log('搜索:', searchTerm);
// 模拟API调用
setSearchResult(`搜索结果: ${searchTerm}`);
}, 500),
[]
);
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
debouncedSearch(newValue);
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="输入搜索关键词..."
/>
<div>{searchResult}</div>
</div>
);
}
五、最佳实践与选择指南
5.1 何时使用受控组件
✅ 使用场景:
- 需要实时验证输入
- 表单提交前需要验证
- 表单字段相互依赖
- 需要动态禁用/启用提交按钮
- 需要强制特定格式的输入
5.2 何时使用非受控组件
✅ 使用场景:
- 文件上传
<input type="file"> - 集成第三方DOM库
- 大型表单性能优化
- 简单的、不需要验证的输入
- 与现有非React代码集成
5.3 性能对比
jsx
// 受控组件:每次输入都重新渲染
function ControlledPerformance() {
const [value, setValue] = useState('');
console.log('受控组件重新渲染'); // 每次输入都会打印
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// 非受控组件:只在需要时获取值
function UncontrolledPerformance() {
const inputRef = useRef();
console.log('非受控组件渲染'); // 只在初始渲染时打印
const getValue = () => {
console.log('获取值:', inputRef.current.value);
};
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={getValue}>获取值</button>
</>
);
}
5.4 自定义Hook封装
jsx
// 自定义受控表单Hook
function useForm(initialValues = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const newValue = type === 'checkbox' ? checked : value;
setValues(prev => ({
...prev,
[name]: newValue
}));
// 清除该字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
};
const resetForm = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
resetForm,
setValues,
setErrors
};
}
// 使用自定义Hook
function SmartForm() {
const {
values,
errors,
touched,
handleChange,
handleBlur,
resetForm
} = useForm({
username: '',
email: ''
});
return (
<form>
<input
name="username"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.username && errors.username && (
<div>{errors.username}</div>
)}
<button type="button" onClick={resetForm}>
重置
</button>
</form>
);
}
总结
核心要点:
- 受控组件:React state作为唯一数据源,实时同步,适合需要验证和控制的场景
- 非受控组件:DOM作为数据源,按需获取,适合文件上传和性能敏感场景
- 混合使用:根据实际需求选择最合适的方式
- 自定义Hook:封装表单逻辑,提高代码复用性
选择建议:
- 大多数情况下,优先使用受控组件,因为更符合React数据流理念
- 文件上传和第三方库集成时,使用非受控组件
- 性能关键的大型表单,可以考虑非受控组件或防抖优化
- 结合实际需求,可以混合使用两种方式
通过理解这两种模式的特点和适用场景,你可以根据具体需求选择最合适的方式,构建出既高效又易维护的React表单应用。