一、非受控组件(Uncontrolled Component)
1. 定义
非受控组件是指表单元素的值由 DOM 自身管理,React 通过 ref 来获取其值。它更接近传统的 HTML 表单行为。
2. 原理
- 使用
useRef创建一个引用(reference)来访问 DOM 元素。 - 在提交表单时,通过
ref.current.value获取输入值。 - 不需要为每个输入设置
onChange和状态。
3. 代码演示
import React, { useRef } from 'react';
function UncontrolledInput() {
// 使用 useRef 创建一个对 input 元素的引用
const inputRef = useRef(null);
// 表单提交时触发的函数
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认的表单提交行为
const value = inputRef.current.value; // 通过 ref 获取 DOM 的值
console.log('提交的值为:', value);
};
return (
<form onSubmit={handleSubmit}>
<label>
用户名:
{/* 使用 ref 绑定到 input 元素 */}
<input type="text" ref={inputRef} />
</label>
<button type="submit">提交</button>
</form>
);
}
4. 优点
- 性能更优:没有频繁的状态更新,适用于对性能敏感的场景。
- 代码简洁:无需为每个字段绑定状态和事件处理函数。
- 接近原生行为:适合熟悉原生表单处理的开发者。
5. 缺点
- 缺乏实时控制:无法在输入时立即获取或修改值。
- 难以实现复杂逻辑:如表单校验、联动、动态更新等。
- 调试困难:数据不在 React 状态中,调试和追踪较难。
- 不支持 React 的某些特性:如状态快照、上下文、状态管理库等。
二、受控组件(Controlled Component)
1. 定义
受控组件是指表单元素的值由 React 组件的状态(state)控制,并通过 onChange 事件来更新状态。换句话说,React 是表单状态的唯一数据源。
2. 原理
- 表单元素的
value属性绑定到 React 的状态。 - 每次用户输入都会触发
onChange事件。 - 事件处理函数中更新状态,从而更新输入框的值。
3. 代码演示
import React, { useState } from 'react';
function ControlledInput() {
// 使用 useState 管理输入框的值和错误信息
const [value, setValue] = useState(''); // 输入框的值
const [error, setError] = useState(''); // 错误提示信息
// 输入变化时触发的函数
const handleChange = (e) => {
const inputValue = e.target.value;
setValue(inputValue); // 更新输入框的值
// 实时校验输入内容长度
if (inputValue.length < 6) {
setError('输入内容不能少于6个字符');
} else {
setError('');
}
};
// 表单提交时触发的函数
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认的表单提交行为
console.log('提交的值为:', value);
};
return (
<form onSubmit={handleSubmit}>
<label>
用户名:
{/* 输入框的 value 由 state 控制 */}
<input
type="text"
value={value}
onChange={handleChange} // 每次输入都会触发 handleChange
/>
</label>
{/* 如果 error 存在,则显示错误提示 */}
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">提交</button>
</form>
);
}
4. 优点
- 数据同步性高:所有输入数据都保存在 React 的状态中,便于统一管理。
- 支持实时校验:可以在用户输入时立即进行格式或内容验证。
- 支持动态更新:输入框的值可以被其他状态或逻辑动态修改。
- 易于重置表单:只需重置状态即可恢复初始值。
- 便于集成其他功能:如表单联动、数据回填、表单校验等。
5. 缺点
- 性能开销:每次输入都会触发
onChange,更新状态,可能带来轻微性能影响(在现代 React 中影响不大)。 - 代码量较大:需要为每个字段设置状态和事件处理函数,代码量相对较多。
- 复杂度高:对于大型表单,管理多个状态和校验逻辑可能变得复杂。
三、对比总结
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React 状态 | DOM 元素 |
| 实时响应 | ✅ 支持 | ❌ 不支持 |
| 表单校验 | ✅ 简单实现 | ❌ 需手动处理 |
| 动态更新 | ✅ 支持 | ❌ 不支持 |
| 性能 | 略低(频繁触发 onChange) | ✅ 更轻量 |
| 适用场景 | 复杂表单、需校验、交互频繁 | 简单输入、性能优先 |
| 代码复杂度 | 较高 | 较低 |
| 可维护性 | ✅ 更好 | ❌ 较差 |
四、适用场景分析
1. 推荐使用受控组件的场景
- 表单需要实时校验(如用户名长度、邮箱格式等)。
- 表单字段之间存在联动(如城市随省份变化而变化)。
- 表单数据需要动态初始化或重置。
- 使用了状态管理库(如 Redux、MobX、Zustand)。
- 需要与 React 的其他特性(如 Context、Hooks、组件通信)集成。
2. 推荐使用非受控组件的场景
- 表单字段简单,仅用于提交数据,无需复杂逻辑。
- 对性能要求较高,且表单交互较少。
- 快速原型开发或临时表单。
- 与第三方库集成时(如某些表单库或富文本编辑器)。
五、选择组件的考虑
1. 表单复杂度决定组件类型
- 简单输入:如搜索框、留言框,可以使用非受控组件。
- 复杂表单:如注册、登录、配置表单,推荐使用受控组件。
2. 是否需要实时交互
- 如果需要在输入时进行校验、提示、联动等操作,使用受控组件。
- 如果只是提交数据,无需复杂交互,可使用非受控组件。
3. 性能考虑
- 对于大型表单或频繁更新的输入,可以结合 防抖(debounce) 和 节流(throttle) 技术优化性能。
- 非受控组件在某些场景下性能更优,但通常差异不大。
六、性能优化建议
1. 使用 useCallback 优化事件处理函数
避免在每次渲染时都创建新的函数,提升性能:
const handleChange = useCallback((e) => {
setValue(e.target.value);
}, []);
想了解useCallback可以看看:React 性能调优必备:React.memo、useCallback、useMemo 在 React 开发中,性能优化 - 掘金
2. 防抖优化输入处理(适用于受控组件)
import { debounce } from 'lodash-es';
const debouncedChange = debounce((e) => {
setValue(e.target.value);
}, 300);
<input type="text" onChange={debouncedChange} />
想了解防抖节流可以看看:那些年我们忽略的高频事件,正在拖垮你的页面高频操作下的性能杀手锏!本文详解防抖与节流原理及实战应用,助你写出更高效、更优 - 掘金
3. 使用表单管理库(如 Formik、React Hook Form)
对于大型表单,推荐使用表单管理库,它们封装了受控组件的复杂性,提供了更简洁的 API 和丰富的功能(如校验、错误提示、提交处理等):
pnpm install react-hook-form
示例代码:
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username', { required: true, minLength: 6 })} />
<button type="submit">登录</button>
</form>
);
}
这个可以去官网学习一下:React Hook Form - 高性能、灵活且可扩展的表单库 - React Hook Form 中文