我不知道的 React:驾驭表单:受控与非受控组件的选择之道
423 字
9 min read
React 表单 受控组件 非受控组件 state ref 性能优化
定义:谁掌控了表单数据?
在 React 中处理表单元素(如 <input>
, <textarea>
, <select>
)时,主要有两种管理其值的方式:受控组件和非受控组件。它们的核心区别在于表单数据的"真理之源"(Source of Truth) 在哪里。
受控组件 (Controlled Components)
定义: 表单元素的值完全由 React 的 state 控制。
- 工作方式:
- 将表单元素的
value
属性绑定到 React 组件的state
。 - 通过
onChange
事件处理器来监听用户的输入。 - 在
onChange
处理器中,调用setState
(或useState
的更新函数) 来更新 state,从而更新 input 的value
。
- 将表单元素的
- 特点: 组件的 state 是表单数据的唯一来源。React 完全接管了表单值的读取和更新,每次输入变化都会流经 React 的状态管理。
- 示例:
import React, { useState } from 'react'; function ControlledInput() { const [inputValue, setInputValue] = useState(''); const handleChange = (event) => { setInputValue(event.target.value); }; return ( <input type="text" value={inputValue} // 值由 state 控制 onChange={handleChange} // 状态随输入更新 /> ); }
- 类比 Vue: Vue 的
v-model
指令提供了类似的双向绑定效果,但 React 的受控组件需要开发者更显式地处理状态更新,提供了更大的灵活性。
非受控组件 (Uncontrolled Components)
定义: 表单数据由 DOM 自身管理,React 不直接控制其值。
- 工作方式:
- React 通常只提供一个初始值,使用
defaultValue
属性 (对于<input>
,<textarea>
,<select>
) 或defaultChecked
(对于 checkbox/radio)。 - 当需要获取表单值时(例如,在表单提交时),通过ref来直接访问 DOM 节点并读取其当前值。
- React 通常只提供一个初始值,使用
- 特点: 数据真理之源在 DOM 中。React 像一个"旁观者",只在需要时才去 DOM "询问"当前的值。
- 示例:
import React, { useRef } from 'react'; function UncontrolledInput() { const inputRef = useRef(null); const handleSubmit = (event) => { event.preventDefault(); alert('Input value: ' + inputRef.current.value); // 通过 ref 读取值 }; return ( <form onSubmit={handleSubmit}> <input type="text" ref={inputRef} defaultValue="初始值" // React 提供初始值 /> <button type="submit">提交</button> </form> ); }
核心区别总结
特性 | 受控组件 | 非受控组件 |
---|---|---|
数据源 | React State | DOM |
值获取 | 直接从 state 读取 | 通过 ref 从 DOM 读取 |
值更新 | onChange + setState |
DOM 自动处理 |
控制粒度 | 精确,每次变化都经过 React | 粗粒度,React 不追踪实时变化 |
代码复杂度 | 稍高,需要管理 state 和事件处理 | 相对简单,尤其对简单表单 |
如何选择:场景驱动决策
选择哪种方式并非绝对好坏,而是取决于具体需求。
何时使用受控组件?
当需要对用户的输入进行实时响应、验证或格式化时,受控组件是首选:
- 实时验证: 输入时即时给出错误提示(如密码强度、邮箱格式)。
// 实时验证输入是否为数字 function NumberInput() { const [value, setValue] = useState(''); const handleChange = (e) => { const inputVal = e.target.value; if (/^\d*$/.test(inputVal)) { // 只允许数字 setValue(inputVal); } }; return <input value={value} onChange={handleChange} />; }
- 动态启用/禁用按钮: 根据表单输入内容是否满足条件来控制提交按钮状态。
- 强制输入格式: 例如,自动将输入转换为大写。
- 依赖输入的衍生 UI: 如根据搜索框输入实时过滤下拉列表。
优势: 数据流清晰可控,状态与 UI 保持同步,易于实现复杂交互和验证逻辑。
何时使用非受控组件?
当表单逻辑简单,或者希望集成非 React 代码/库时,非受控组件更便捷:
- 简单表单: 只需要在提交时获取一次最终值,中间过程无需干预。
- 一次性提交: 如简单的登录、注册表单。
- 文件上传 (
<input type="file">
): 文件输入框的值只能由用户设置,无法通过程序设置value
,因此它总是非受控的。function FileUpload() { const fileInputRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); if (fileInputRef.current.files.length > 0) { alert(`Selected file - ${fileInputRef.current.files[0].name}`); } }; return ( <form onSubmit={handleSubmit}> <input type="file" ref={fileInputRef} /> <button type="submit">上传</button> </form> ); }
- 集成第三方 UI 库: 有些库可能直接操作 DOM,使用非受控组件更容易集成。
优势: 代码量少,更接近传统 HTML 表单行为,性能开销相对较小(因为不涉及每次输入都触发 React 更新)。
设计考量与性能优化
选择模式时,还需要考虑性能和维护性。
受控组件的性能考量
- 潜在问题: 受控组件的每一次按键输入都会触发
setState
(或useState
更新),导致组件及其子树的重渲染 (Re-render)。对于包含大量输入框或复杂计算的表单,这可能成为性能瓶颈。 - 优化手段:
- 防抖/节流 (Debounce/Throttle): 对于不需要实时响应的受控输入(如搜索建议),可以使用防抖或节流来限制状态更新和重渲染的频率。
- 组件拆分与
React.memo
: 将表单拆分成更小的组件,并使用React.memo
避免因子组件 Props 未变而触发的不必要渲染。 - 性能分析: 使用 React DevTools Profiler 识别哪些输入或组件更新开销较大。
混合策略:取长补短
在复杂场景下,有时可以结合使用两种模式:
- 场景: 一个表单可能大部分字段是非受控的(提交时获取),但某个字段需要实时验证(受控)。
- 实现: 可以将需要受控的字段作为受控组件管理,其他字段使用
ref
在提交时读取。
结论与建议
受控组件和非受控组件是 React 提供的两种管理表单数据的有效方式。
- 受控组件: 提供完全的数据控制能力和实时响应性,适用于需要精确控制、验证或动态交互的场景,但需注意潜在的性能开销。
- 非受控组件: 更接近原生 HTML 表单,代码简洁,性能开销小,适用于简单表单、文件上传或集成第三方库的场景,但牺牲了实时控制能力。
选择建议:
- 优先考虑受控组件: 在大多数需要与 React 状态交互的情况下,受控组件提供了更清晰、可预测的数据流,是 React 的推荐方式。
- 简单场景用非受控: 对于非常简单的表单,如果不需要实时验证或交互,非受控组件可以减少样板代码。
- 性能敏感时评估: 如果受控组件导致性能问题,考虑优化(防抖、
memo
)或在特定字段上切换到非受控方式(通过ref
读取)。 - 保持一致性: 在项目中尽量保持风格统一,有助于维护。如果团队决定主要使用受控组件,就在大部分场景下坚持使用。
理解两者的核心差异和适用场景,是构建高效、可维护 React 表单的关键。