受控组件和非受控组件的理解
大约 6 分钟
受控组件与非受控组件详解
1. 基本概念
受控组件(Controlled Components)
- 定义:表单数据由 React 组件的 state 管理
- 特点:组件的值完全由 props 或 state 控制
- 数据流:单向数据流(React → DOM)
非受控组件(Uncontrolled Components)
- 定义:表单数据由 DOM 元素自身管理
- 特点:使用 ref 直接获取 DOM 元素的值
- 数据流:传统的双向数据绑定方式
2. 受控组件详解
2.1 基本示例
import React, { useState } from 'react';
// 受控组件示例
function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交数据:', { name, email, age });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
value={name} // 值由 state 控制
onChange={(e) => setName(e.target.value)} // 通过事件更新 state
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label>Age:</label>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
/>
</div>
<button type="submit">Submit</button>
{/* 实时显示表单数据 */}
<div>
<h3>当前数据:</h3>
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Age: {age}</p>
</div>
</form>
);
}2.2 受控组件的优势
// 1. 即时验证
function ValidatedInput() {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
// 即时验证
if (newValue.length < 3) {
setError('长度至少为3个字符');
} else {
setError('');
}
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
style={{ borderColor: error ? 'red' : 'green' }}
/>
{error && <span style={{ color: 'red' }}>{error}</span>}
</div>
);
}
// 2. 条件禁用
function ConditionalForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const isFormValid = username.length >= 3 &&
password.length >= 6 &&
password === confirmPassword;
return (
<form>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input
type="password"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<button
type="submit"
disabled={!isFormValid} // 根据状态动态禁用
>
Register
</button>
</form>
);
}3. 非受控组件详解
3.1 基本示例
import React, { useRef } from 'react';
// 非受控组件示例
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const ageRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
// 通过 ref 获取值
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
age: ageRef.current.value
};
console.log('提交数据:', formData);
// 清空表单
e.target.reset();
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
ref={nameRef} // 使用 ref 引用
defaultValue="" // 初始值
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
ref={emailRef}
defaultValue=""
/>
</div>
<div>
<label>Age:</label>
<input
type="number"
ref={ageRef}
defaultValue=""
/>
</div>
<button type="submit">Submit</button>
</form>
);
}3.2 使用 defaultValue 和 defaultChecked
function UncontrolledWithDefaults() {
const selectRef = useRef();
const checkboxRef = useRef();
const radioRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log({
select: selectRef.current.value,
checkbox: checkboxRef.current.checked,
radio: radioRef.current.value
});
};
return (
<form onSubmit={handleSubmit}>
{/* 下拉选择 */}
<select ref={selectRef} defaultValue="option2">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
{/* 复选框 */}
<input
type="checkbox"
ref={checkboxRef}
defaultChecked={true}
/>
{/* 单选按钮 */}
<input
type="radio"
name="radioGroup"
value="A"
defaultChecked
/>
<input
type="radio"
name="radioGroup"
value="B"
ref={radioRef}
/>
<button type="submit">Submit</button>
</form>
);
}4. 文件上传示例
4.1 非受控文件输入(推荐)
function FileUpload() {
const fileInputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const file = fileInputRef.current.files[0];
if (file) {
console.log('选择的文件:', file.name);
// 处理文件上传
const formData = new FormData();
formData.append('file', file);
// 上传逻辑...
}
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
console.log('文件已选择:', file.name);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
/>
<button type="submit">Upload</button>
</form>
);
}4.2 受控文件输入(不推荐)
// 注意:文件输入通常不建议使用受控方式
function ControlledFileUpload() {
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
setFile(selectedFile);
};
const handleSubmit = (e) => {
e.preventDefault();
if (file) {
console.log('上传文件:', file.name);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="file"
onChange={handleFileChange}
// 注意:value 属性对文件输入无效
/>
<button type="submit">Upload</button>
</form>
);
}5. 性能对比
5.1 受控组件性能考虑
// 可能的性能问题
function PerformanceIssue() {
const [text, setText] = useState('');
// 每次输入都会触发重新渲染
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input value={text} onChange={handleChange} />
<ExpensiveComponent /> {/* 每次输入都会重新渲染这个昂贵组件 */}
</div>
);
}
// 优化方案
const OptimizedForm = React.memo(({ onTextChange }) => {
const [text, setText] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setText(value);
onTextChange(value); // 只通知父组件变化
};
return (
<input value={text} onChange={handleChange} />
);
});5.2 非受控组件性能优势
// 非受控组件减少重新渲染
function BetterPerformance() {
const inputRef = useRef();
// 只在需要时读取值,不触发重新渲染
const handleSubmit = () => {
const value = inputRef.current.value;
console.log('提交值:', value);
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
<ExpensiveComponent /> {/* 不会被输入操作影响 */}
</div>
);
}6. 实际应用场景
6.1 受控组件适用场景
// 1. 表单验证
function ValidationForm() {
const [email, setEmail] = useState('');
const [isValid, setIsValid] = useState(true);
const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
const handleEmailChange = (e) => {
const value = e.target.value;
setEmail(value);
setIsValid(validateEmail(value));
};
return (
<div>
<input
type="email"
value={email}
onChange={handleEmailChange}
style={{ borderColor: isValid ? 'green' : 'red' }}
placeholder="Enter email"
/>
{!isValid && <span style={{ color: 'red' }}>请输入有效的邮箱地址</span>}
</div>
);
}
// 2. 动态表单
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }]);
const addField = () => {
setFields([...fields, { id: Date.now(), value: '' }]);
};
const updateField = (id, value) => {
setFields(fields.map(field =>
field.id === id ? { ...field, value } : field
));
};
return (
<div>
{fields.map(field => (
<input
key={field.id}
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
placeholder={`Field ${field.id}`}
/>
))}
<button onClick={addField}>Add Field</button>
</div>
);
}6.2 非受控组件适用场景
// 1. 简单的数据收集
function SimpleDataCollection() {
const nameRef = useRef();
const emailRef = useRef();
const handleSubmit = () => {
// 一次性获取所有数据
const data = {
name: nameRef.current.value,
email: emailRef.current.value
};
console.log('收集到的数据:', data);
};
return (
<div>
<input ref={nameRef} placeholder="Name" />
<input ref={emailRef} placeholder="Email" />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
// 2. 与第三方库集成
function ThirdPartyIntegration() {
const inputRef = useRef();
useEffect(() => {
// 第三方库直接操作 DOM
const autoComplete = new AutoCompleteLibrary(inputRef.current);
return () => {
autoComplete.destroy();
};
}, []);
const handleSubmit = () => {
// 获取第三方库处理后的值
const value = inputRef.current.value;
console.log('自动完成的值:', value);
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleSubmit}>Get Value</button>
</div>
);
}7. 混合使用示例
// 在同一个表单中混合使用
function HybridForm() {
// 受控组件:需要实时验证的字段
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
// 非受控组件:只需提交时获取的字段
const nameRef = useRef();
const messageRef = useRef();
const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
const handleEmailChange = (e) => {
const value = e.target.value;
setEmail(value);
setEmailError(validateEmail(value) ? '' : '无效邮箱地址');
};
const handleSubmit = (e) => {
e.preventDefault();
// 验证受控字段
if (emailError) {
alert('请修正表单错误');
return;
}
// 收集所有数据
const formData = {
name: nameRef.current.value,
email: email,
message: messageRef.current.value
};
console.log('提交数据:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input ref={nameRef} />
</div>
<div>
<label>Email:</label>
<input
value={email}
onChange={handleEmailChange}
style={{ borderColor: emailError ? 'red' : 'initial' }}
/>
{emailError && <span style={{ color: 'red' }}>{emailError}</span>}
</div>
<div>
<label>Message:</label>
<textarea ref={messageRef} />
</div>
<button type="submit">Submit</button>
</form>
);
}8. 选择建议
何时使用受控组件:
- ✅ 需要实时验证表单数据
- ✅ 需要根据输入动态更新 UI
- ✅ 需要格式化或限制用户输入
- ✅ 需要在多个组件间共享表单状态
- ✅ 需要完全控制表单行为
何时使用非受控组件:
- ✅ 表单很简单,不需要实时控制
- ✅ 与第三方库集成
- ✅ 性能敏感的场景
- ✅ 文件上传等特殊输入类型
- ✅ 快速原型开发
9. 总结
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据源 | React state/props | DOM 元素自身 |
| 实时控制 | ✅ 完全控制 | ❌ 无法实时控制 |
| 性能 | 可能频繁重渲染 | 性能更好 |
| 代码复杂度 | 较高 | 较低 |
| 验证 | ✅ 实时验证 | ❌ 提交时验证 |
| 适用场景 | 复杂表单 | 简单数据收集 |
选择受控还是非受控组件主要取决于具体需求,理解两者的差异有助于做出合适的技术决策。