useRef 深度解析:DOM 引用与受控、非受控表单的取舍
在 React Hooks 中,useRef 常被低估。它不像 useState 那样直接驱动渲染,却在许多场景中不可或缺。本文聚焦 useRef 的本质,结合表单控制,清晰对比受控与非受控组件,帮助你明白“什么时候用 useRef,为什么这样选”。
1. useRef 的本质
const ref = useRef(initialValue);
useRef 返回一个可变对象 { current: initialValue },具备三个关键特性:
- 可直接修改
.current - 修改不触发组件渲染
- 对象在组件整个生命周期保持不变
核心结论:useRef 不是用来驱动 UI,而是用来“持久存储”数据的容器。
2. useRef vs useState:根本区别
| 对比点 | useState | useRef |
|---|---|---|
| 修改是否触发渲染 | 是 | 否 |
| 是否响应式 | 是 | 否 |
| 适合驱动 UI | 是 | 否 |
| 适合存储中间状态 | 否(会引发不必要渲染) | 是 |
简言之:useState 管理“状态”(影响视图),useRef 管理“存储”(不影响视图)。
3. useRef 典型场景
(1)直接访问 DOM 元素
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
适用于:自动聚焦、滚动定位、测量尺寸等命令式操作。
(2)存储不影响渲染的可变值
const timerId = useRef(null);
const start = () => {
timerId.current = setInterval(() => {}, 1000);
};
const stop = () => {
clearInterval(timerId.current);
};
若用 useState 存储 timerId,会导致无意义渲染。useRef 更清晰、更高效。
4. 表单控制:受控组件 vs 非受控组件
(1)受控组件
const [value, setValue] = useState('');
<input value={value} onChange={e => setValue(e.target.value)} />;
- 值完全由 React state 控制
- 每次输入都触发渲染
优势:
- 实时校验、格式化、联动
- 状态可预测、易调试
代价:
- 频繁渲染,字段多时性能消耗增大
适合:复杂表单(如注册、订单填写)需要实时控制的场景。
(2)非受控组件
const inputRef = useRef(null);
// 提交时读取
const value = inputRef.current.value;
<input ref={inputRef} />;
- 值由 DOM 自身维护
- React 只在需要时读取
优势:
- 无频繁渲染,性能更好
- 代码简洁
局限:
- 无法实时校验或联动
- 状态不可预测
适合:简单一次性提交场景(如搜索框、评论提交、文件上传)。
(3)受控 vs 非受控 快速对比
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 值来源 | React state | DOM 自身 |
| 是否实时响应 | 是 | 否 |
| 性能 | 一般 | 更好 |
| 适合场景 | 复杂交互、实时校验 | 简单提交、一次性读取 |
选择原则:
需要实时“控制”输入过程 → 受控组件
只需在提交时“获取”结果 → 非受控组件(借助 useRef)
5. useRef 在表单体系中的定位
useRef 不是 state 的替代品,而是:
- 提供命令式 DOM 操作的官方入口
- 为非受控组件实现“按需读取”
- 存储不参与渲染的中间数据
实际项目中,受控与非受控往往混合使用,useRef 起到桥梁作用。
6. 总结
- useRef 是不会触发渲染的可变容器,专用于存储 DOM 引用、定时器、中间变量等。
- 受控组件以状态驱动 UI,适合复杂实时交互;非受控组件让 DOM 自治,适合简单高效提交。
- 核心决策点只有一个:这个值是否需要驱动视图渲染?
需要 → 用 useState(受控)
不需要 → 用 useRef(非受控或存储)