深入理解 React 中的 useRef:从 DOM 引用到受控/非受控组件的设计哲学
在 React 函数组件的世界里,useRef 看似简单,却承载着连接“声明式 UI”与“命令式操作”的关键桥梁作用。它不仅是获取 DOM 元素的工具,更是实现非受控组件、管理可变状态、绕过闭包陷阱的核心手段。
而要真正掌握 useRef 的价值,必须将其置于 受控组件(Controlled Components) 与 非受控组件(Uncontrolled Components) 的对比框架中理解。本文将系统讲解:
useRef的本质与特性- 受控 vs 非受控组件的核心区别
- 何时该用哪种模式?
一、useRef 是什么?——一个持久化的“容器”
const ref = useRef(initialValue);
- 返回一个对象
{ current: initialValue } - 引用地址在整个组件生命周期中不变
- 修改
ref.current不会触发重渲染 - 适用于存储不需要驱动 UI 更新的数据
🧠 你可以把
useRef理解为函数组件中的“实例属性”(类似类组件中的this.xxx)。
useRef 与 useState:React 状态体系的“双子星”
在函数组件中,useState 和 useRef 共同构成了管理数据的两大支柱,但它们的职责截然不同:
| 特性 | useState | useRef |
|---|---|---|
| 目的 | 管理响应式状态,驱动 UI 更新 | 存储非响应式可变值,不触发重渲染 |
| 更新方式 | 必须通过 setter 函数(如 setValue) | 直接赋值(ref.current = newValue) |
| 重渲染 | ✅ 触发 | ❌ 不触发 |
| 适用场景 | 用户输入、UI 状态、条件渲染 | DOM 引用、定时器 ID、上一次值、闭包逃逸 |
关键区别示例
const [count, setCount] = useState(0); // 修改后组件重新渲染
const countRef = useRef(0); // 修改后组件静默更新
// 正确用法
setCount(count + 1); // ✅ 触发 UI 更新
countRef.current = countRef.current + 1; // ✅ 仅内部记录,不影响 UI
💡 记住:
- 如果这个值的变化需要用户看到 → 用
useState- 如果这个值只是程序内部使用 → 用
useRef
二、受控组件 vs 非受控组件:React 表单的两种范式
1. 受控组件(Controlled Component)
定义:表单元素的值由 React 状态(state)完全控制。
import { useState } from 'react';
export default function LoginForm() {
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form); // 状态即表单数据
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={form.username}
onChange={handleChange}
placeholder="用户名"
/>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}
✅ 优点:
- 数据完全由状态驱动,符合 React 单向数据流
- 易于实现实时校验、输入联动(如密码强度提示)
- 可预测、可调试、可测试
❌ 缺点:
- 每次输入都触发重渲染(虽有优化,但仍有成本)
- 代码略显冗长(需写
onChange+value)
💡 适用场景:需要实时反馈、复杂校验、动态表单(如多步骤表单、字段依赖)
✅ 为什么必须用 useState?
因为:
- 输入框的显示内容必须随状态变化而更新;
- 任何校验、联动、重置逻辑都依赖于状态的一致性;
- React 的单向数据流要求“状态是唯一真相源”。
2. 非受控组件(Uncontrolled Component)
定义:表单元素的值由 DOM 自身管理,React 通过 ref 在需要时读取其值。
import { useRef } from 'react';
export default function CommentBox() {
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current?.value;
if (!comment) return alert('请输入评论');
console.log(comment); // 从 DOM 直接读取
};
return (
<div>
<textarea ref={textareaRef} placeholder="请输入评论" />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
✅ 优点:
- 无需为每个输入绑定状态,性能更优(尤其在大型表单中)
- 代码简洁,适合一次性读取场景
- 与原生 HTML 行为一致,学习成本低
❌ 缺点:
- 无法实现实时校验或输入联动
- 调试困难(值不在 React 状态树中)
- 不符合 React 声明式理念(混合了命令式操作)
⚠️ 注意:这里不能用 useState,因为:
- 我们不关心输入过程中的中间值;
- 不希望每次按键都触发重渲染;
- 最终只需一次读取(如提交时)。
💡 适用场景:简单表单、文件上传(
<input type="file">必须是非受控)、一次性提交(如搜索框、评论框)
三、useRef 的三大核心应用场景
场景 1:实现非受控组件(DOM 值读取)
这是 useRef 最直接的用途——让 React “放手”表单控制权,仅在提交时介入。
const inputRef = useRef(null);
const onSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value); // 读取当前值
};
⚠️ 注意:
<input type="file">必须是非受控的,因为其value是只读的。
场景 2:在受控组件中辅助命令式操作
即使使用受控组件,有时仍需命令式操作,比如自动聚焦:
const [value, setValue] = useState('');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // 聚焦,但值仍由 state 控制
}, []);
return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />;
这里,value 是受控的,但 focus() 是命令式的——两者可以共存。
场景 3:混合模式 —— 同一表单中同时使用受控与非受控
export default function App() {
const [username, setUsername] = useState(''); // 受控
const passwordRef = useRef(null); // 非受控
const handleSubmit = (e) => {
e.preventDefault();
console.log('用户名:', username);
console.log('密码:', passwordRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名(受控)"
/>
<input
ref={passwordRef}
type="password"
placeholder="密码(非受控)"
/>
<button type="submit">登录</button>
</form>
);
}
✅ 这种混合模式在实际项目中很常见:关键字段用受控(如用户名),敏感字段用非受控(如密码)以减少状态暴露。
四、如何选择?受控 or 非受控?—— useState 还是 useRef?
| 需求 | 推荐方案 | 使用的 Hook |
|---|---|---|
| 实时校验邮箱格式 | 受控组件 | useState |
| 提交评论(无需预览) | 非受控组件 | useRef |
| 密码强度提示 | 受控组件 | useState |
| 文件上传 | 非受控组件 | useRef |
| 表单重置功能 | 受控组件(更易实现) | useState |
| 高频输入(如搜索建议) | 受控 + 防抖 | useState + useRef(存最新值) |
🎯 经验法则:
- 表单需要即时反馈 → 用受控
- 表单只需最终提交 → 用非受控
- 不确定?优先用受控,它是 React 推荐的模式
五、常见误区与最佳实践
❌ 误区1:在非受控组件中设置 value 属性
// 错误!这会让 input 变成“只读”
<input ref={ref} value="固定值" />
应使用 defaultValue:
<input ref={ref} defaultValue="初始值" />
❌ 误区2:用 useRef 存储需要驱动 UI 的数据
const countRef = useRef(0);
// ...
{countRef.current} // ❌ 不会更新 UI!
✅ 正确做法:用 useState。
✅ 最佳实践:封装自定义 Hook 统一管理
// useInput.js
import { useState } from 'react';
export const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
return {
value,
onChange: (e) => setValue(e.target.value),
reset: () => setValue(initialValue)
};
};
// 使用
const username = useInput('');
<input {...username} />;
六、总结:useState 是“大脑”,useRef 是“手脚”
-
useState是 React 响应式系统的核心,负责维护 UI 状态的一致性; -
useRef是 React 对命令式世界的妥协与赋能,用于处理 DOM、存储瞬态数据; -
在表单设计中:
- 受控 =
useState主导 → 数据驱动、可预测; - 非受控 =
useRef主导 → 性能优先、简单直接;
- 受控 =
-
两者不是对立,而是互补:复杂应用往往同时使用二者。
🌟 终极建议:
“Use state for what the user sees, refs for what the program needs.”
—— 用户看到的用状态,程序需要的用引用。
掌握这一原则,你就能在 React 的声明式世界与现实的命令式需求之间,游刃有余。