useRef vs useState:React 中“沉默的守护者”与“高调的响应者”
在 React 的声明式世界里,UI 是状态的函数——状态变,界面随之更新。而驱动这一机制的核心,正是我们熟悉的 useState。但你是否注意到,有些数据明明在变化,却不会引起任何重新渲染?它们安静地存在于组件之中,不惊动 React 的调度系统,却在关键时刻发挥作用。
这就是 useRef 的领地。
尽管 useState 和 useRef 都能“存值”,但它们的设计哲学截然不同:
useState是 React 数据流的心脏,每一次更新都会触发 UI 同步;useRef则是 React 提供的“脱围机制” (escape hatch),让你在必要时绕过声明式规则,直接与 DOM 或外部系统交互。
更有趣的是,这两种能力也延伸出了表单处理的两种范式:受控组件 与 非受控组件——一个由 React 全面掌控,一个以 DOM 为单一数据源。
本文将通过清晰的对比、真实的代码示例和生动的比喻,带你深入理解:
- 它们各自的本质与适用边界;
- 何时该用“高调的响应者”,何时该请出“沉默的守护者”;
- 如何在实际项目中做出合理选择,写出更符合 React 思维的代码。
准备好了吗?让我们揭开这对“双生 Hook”的神秘面纱。
一、初识两位“角色”
🎭 useState:高调的响应者
useState 是 React 中最广为人知的状态管理工具。当你调用 setCount(count + 1),组件会立即重新渲染,UI 随之更新。
它就像一位“高调的响应者”——一旦数据变化,立刻通知整个舞台(组件)进行重演(re-render)。
const [count, setCount] = useState(0);
关于useState的介绍在这里就不赘述了,在我之前的文章中也有详细介绍过。
✅ 特点:响应式、触发重渲染、用于驱动 UI 变化。
🕵️ useRef:沉默的守护者
ref是React提供的一种脱围机制,何为脱围机制?脱围” (也叫“逃逸”或“绕过 React 的声明式机制”)指的是绕过 React 的常规数据流和渲染机制,直接与 DOM 或其他外部系统进行交互。
使用useRef定义的数据发生改变不会触发页面的重新渲染,useRef会返回这样一个对象
{
current:""//你向useRef传递的值
}
你可以用 ref.current 属性访问该 ref 的当前值。这个值是有意被设置为可变的,意味着你既可以读取它也可以写入它。就像一个 React 追踪不到的、用来存储组件信息的秘密“口袋”
相比之下,useRef 更像一位“沉默的守护者”。它也能保存值,但不会触发组件重渲染。无论你如何修改 ref.current,React 都视而不见——除非你主动在副作用或事件处理器中读取或操作它。
const inputRef = useRef(null);
✅ 特点:非响应式、持久化引用、不触发重渲染、适合存储“中间状态”或 DOM 引用。
二、对比:相同点与关键差异
| 特性 | useRef | useState |
|---|---|---|
| 返回值 | 返回一个对象 { current: initialValue },你可以直接修改 .current | 返回一个数组 [value, setValue],其中 setValue 是唯一修改状态的方式 |
| 是否触发重渲染 | ❌ 不会触发重渲染 | ✅ 会触发组件重新渲染 |
| 可变性 | ✅ 可变:可在渲染外任意修改 .current 值 | ⚠️ “不可变”:必须通过 setter 修改,触发调度机制 |
| 渲染期间访问 | ⚠️ 不应在渲染期间读取或写入 ref.current,否则可能导致 UI 与实际值不一致 | ✅ 可随时读取,但每次渲染都有自己的“state 快照” |
💡 补充说明:
ref的“当前值”不是“状态” :它只是一个普通的 JavaScript 对象,没有参与 React 的 diff 和 re-render 流程。state是“快照式”的:每次渲染都会生成一份独立的state快照,确保 UI 与数据同步。ref是“持久引用” :即使组件重新渲染,ref.current依然指向同一个对象,适合保存定时器 ID、DOM 元素等跨周期数据。
三、实战场景解析
场景 1:自动聚焦输入框(访问 DOM)
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 直接操作 DOM
}, []);
return <input ref={inputRef} />;
这里必须使用 useRef:
- 因为我们需要直接访问 DOM 元素;
- 如果用
useState存储 DOM 引用,不仅无法实现,还会因不必要的状态更新导致性能浪费。
🔍 比喻:
useRef就像一把“钥匙”,默默保管着通往真实 DOM 的门锁,只有你需要时才拿出来用,不会惊动整个房间(组件)。
场景 2:保存定时器 ID(持久化可变值)
import {
useRef,
useState ,
useEffect,
} from 'react'
export default function App(){
// let intervalId =null;//count改变就被覆盖
let intervalId =useRef(null);//count改变不会被覆盖
const [count,setCount] = useState(0);
function start(){
intervalId.current = setInterval(()=>{
console.log('tick');
},1000)
console.log(intervalId);
}
function stop(){
clearInterval(intervalId.current)
}
return(
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
{count}
<button type="button" onClick={()=>setCount(count+1)}>count+1</button>
</>
)
}
为什么不用普通变量 let intervalId = null?
因为每次组件 re-render(比如 count 改变),变量都会被重置为 null,导致无法清除之前的定时器!所以就无法像我们预料的那样点击停止后关闭定时器
而 useRef 提供了一个跨渲染周期持久存在的可变容器,就像一个挂在外墙上的工具箱——无论房间(组件)如何翻新(re-render),里面的工具(.current)始终可用。
💡 何时该用
useRef存值?当且仅当满足:
- 该值不用于计算 JSX(即不影响 UI);
- 你需要在多次渲染之间保持其引用不变。
否则,请优先考虑
useState。
四、受控 vs 非受控组件 —— 表单的两种哲学
在 React 中处理表单时,我们常面临一个选择:
让 React 完全掌控表单状态(受控),还是 以 DOM 为单一数据源,仅在需要时读取(非受控)?
这背后其实是两种不同的数据流哲学。
🎯 受控组件:React 主导一切
//<LoginForm />
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
return (
<>
<input
type="text"
name="username"
value={form.username}
onChange={handleChange}
/>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
/>
</>
);
- 输入框的
value由状态form.username完全控制; - 用户每次输入 → 触发
onChange→ 更新状态 → 触发重渲染 → 显示新值; - 数据流是单向的:状态 → UI,用户输入 → 状态更新。如果不添加onChange事件,我们甚至无法在表单中进行输入。
✅ 优点:
- 实时校验(如密码强度提示)
- 表单联动(如根据用户名自动填充邮箱)
- 状态可预测,便于调试和测试
💡 比喻:受控组件就像一位“严格教练”,学员(输入框)的每一个动作都必须经过他的指令(状态)才能执行。
🕊️ 非受控组件:以 DOM 为单一数据源
//<CommentBox />
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current.value; // 直接读取 DOM 值
textareaRef.current.value = ''; // 手动清空
};
return <textarea ref={textareaRef} />;
<textarea>没有绑定value,其值由 DOM 自身维护;- 提交时通过
ref.current.value一次性读取当前值; - React 不参与输入过程的状态同步。
✅ 优点:
- 代码更简洁(无需为每个字段写状态和 handler)
- 性能略优(避免频繁
setState)- 适合简单表单或一次性操作(如评论、搜索框)
⚠️ 注意:非受控组件无法实现实时验证或动态联动,因为 React “看不见”输入过程。
💡 比喻:非受控组件像一位“自由学生”,平时自己练习(DOM 管理值),只在考试时(提交)把结果交给老师(React)。
🔍 如何选择?
| 场景 | 推荐方式 |
|---|---|
| 复杂表单(校验、联动、动态字段) | ✅ 受控组件(useState + onChange) |
| 简单输入(如评论、一次性搜索) | ✅ 非受控组件(useRef + ref.current.value) |
文件上传 <input type="file"> | ⚠️ 必须用非受控(因 value 是只读的) |
| 表单中同时包含简单字段和复杂字段 | ✅ 混合使用:简单字段用非受控,复杂字段用受控 |
📌 关键原则:
- 如果你需要响应用户的每一次输入 → 用受控;
- 如果你只关心最终提交的值 → 用非受控。
五、何时该用谁?(完整决策指南)
| 需求 | 推荐方案 |
|---|---|
| 数据变化需要更新 UI | ✅ useState(受控组件) |
| 需要访问或操作 DOM 元素 | ✅ useRef |
| 存储临时值(如 timer ID、WebSocket 实例) | ✅ useRef |
| 表单需实时校验、联动或动态控制 | ✅ 受控组件(useState) |
| 简单表单,只需提交时读值 | ✅ 非受控组件(useRef) |
| 避免因频繁更新导致性能问题 | ✅ useRef(当值无需触发渲染时) |
六、总结:工具无好坏,场景定乾坤
useState是 UI 的“指挥官” :它掌控界面状态,一有风吹草动就下令重绘。useRef是后台的“工程师” :它默默维护那些不需要观众(用户)知道的细节,比如 DOM 引用、定时器、滚动位置等。- 受控组件追求精确控制,适合复杂交互;
- 非受控组件追求简洁高效,适合轻量场景。
正如真实项目中往往同时存在 <CommentBox />(非受控)和 <LoginForm />(受控),高明的开发者懂得:不是所有问题都需要“响应式”答案。
🌟 终极心法(来自 React 哲学):
优先使用声明式编程(useState+ 受控组件)来描述 UI 应该是什么样子;
仅在必要时使用命令式操作(useRef+ 非受控组件)来处理 React 无法声明式表达的场景。
在“描述意图”与“直接操作”之间,找到最符合 React 思维的平衡。