在 React Hooks 体系中,useState 是最常用的状态管理工具,但它有一个特性:每次状态变化都会触发组件重渲染。如果我们只是想“保存一个可变值”而不希望引起渲染,就需要另一个 Hook —— useRef。
useRef 返回一个可变的 ref 对象,其 .current 属性可以存储任意值,并且在整个组件生命周期内保持引用不变,最重要的是:改变 .current 不会触发重渲染。这使得它成为 React 中处理 DOM 操作、持久化变量、性能优化的利器。
本文将结合多个真实场景代码示例,系统讲解 useRef 的用法、与 useState 的区别,以及在受控/非受控组件中的应用,帮助你彻底掌握这个“默默奉献”的 Hook。
useRef 基础:创建一个持久化的可变引用
jsx
import { useRef } from 'react';
function App() {
const inputRef = useRef(null); // 初始值 null
return <input ref={inputRef} />;
}
- useRef(initialValue) 创建一个 ref 对象。
- 返回的对象结构:{ current: initialValue }。
- .current 可在整个生命周期内随意读写,且不会触发渲染。
- 常与 ref 属性配合,用于获取 DOM 节点。
场景一:DOM 操作 —— 自动聚焦输入框
这是 useRef 最经典的应用:获取 DOM 元素并调用原生方法。
jsx
import { useRef, useEffect } from 'react';
export default function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 组件挂载后自动聚焦
}, []);
return <input ref={inputRef} placeholder="自动获得焦点" />;
}
相当于 Vue 中的 onMounted(() => inputEl.focus())。
为什么不用 useState?因为我们不需要根据焦点状态来渲染任何 UI,单纯的操作 DOM 而已,使用 useRef 更合适。
场景二:持久化变量 —— 定时器 ID 管理
在处理 setInterval、setTimeout、WebSocket 等需要清理的副作用时,我们常常需要保存一个可变值,并在组件卸载或多次点击时访问它。
jsx
import { useRef, useState } from 'react';
export default function App() {
const intervalId = useRef(null);
const [count, setCount] = useState(0);
const start = () => {
if (intervalId.current) return; // 防止重复启动
intervalId.current = setInterval(() => {
console.log('tick~~~~~');
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
if (intervalId.current) {
clearInterval(intervalId.current);
intervalId.current = null;
}
};
return (
<>
<p>计数:{count}</p>
<button onClick={start}>开始计时</button>
<button onClick={stop}>停止计时</button>
</>
);
}
为什么不用 useState 存储 intervalId?
- 如果用 useState,每次 setIntervalId 都会触发重渲染(即使值没变)。
- 更严重的是:函数组件每次渲染都会重新执行,如果在渲染中访问旧的 id,可能出现闭包陷阱。
- useRef 确保 .current 始终指向最新值,且修改不触发渲染,完美适合这种场景。
useRef vs useState:核心区别对比
| 特性 | useState | useRef |
|---|---|---|
| 返回值 | [state, setState] | { current: value } |
| 修改是否触发渲染 | 是 | 否 |
| 典型用途 | 驱动 UI 渲染的状态 | 保存不影响渲染的可变值、DOM 引用 |
| 闭包安全性 | 需注意 stale closure | .current 始终是最新的 |
| 初始值 | 传入初始值 | 传入初始值(仅在首次渲染使用) |
简单记忆:需要渲染用 useState,不需要渲染用 useRef。
场景三:受控组件 vs 非受控组件 —— 表单值获取方式
表单处理是 React 中另一个常见战场,useRef 在这里大放异彩。
受控组件(Controlled Components)
值由 React 状态控制,实时同步,适合需要即时验证、联动、展示的场景。
jsx
import { useState } from 'react';
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
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}
优点:数据始终与状态一致,便于校验、禁用按钮、实时预览等。
缺点:每次输入都触发重渲染(可通过防抖优化)。
非受控组件(Uncontrolled Components)
值由 DOM 自身管理,通过 ref 在需要时读取,适合一次性提交、文件上传等场景。
jsx
import { useRef } from 'react';
function CommentBox() {
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current.value.trim();
if (!comment) {
alert('请输入评论');
return;
}
console.log('提交评论:', comment);
textareaRef.current.value = ''; // 清空
};
return (
<div>
<textarea ref={textareaRef} placeholder="写下你的评论..." />
<button onClick={handleSubmit}>提交评论</button>
</div>
);
}
优点:性能更好(无频繁渲染),代码更简洁。
缺点:无法实时获取值或做即时验证。
混合使用示例
实际项目中两者经常结合:
jsx
import { useState, useRef } from 'react';
function App() {
const [value, setValue] = useState(''); // 受控:实时显示输入内容
const inputRef = useRef(null); // 非受控:提交时获取原生值
const handleSubmit = (e) => {
e.preventDefault();
console.log('受控值:', value);
console.log('非受控值:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<p>你输入了:{value}</p>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<input ref={inputRef} placeholder="这个不会实时显示" />
<button type="submit">提交</button>
</form>
);
}
最佳实践与注意事项
-
优先使用受控组件:React 官方推荐,大多数表单场景应使用受控方式,便于状态管理。
-
非受控适用于特定场景:
- 文件上传( 无法受控)
- 与第三方库集成(如富文本编辑器)
- 性能极致优化的大表单
-
避免滥用 useRef 存储大量状态:如果值需要驱动渲染,请用 useState 或其他状态管理方案。
-
useRef 初始值只在首次渲染使用:后续渲染不会重新赋值。
-
清理副作用:在 useEffect 返回清理函数中清除 ref 存储的定时器、订阅等。
结语
useRef 虽然不像 useState 那样耀眼,但它在 React 开发中扮演着不可或缺的角色:
- 它让我们能安全地操作 DOM。
- 它为定时器、订阅等副作用提供了可靠的存储容器。
- 它是非受控组件的核心支撑。
- 它帮助我们避免不必要的重渲染,提升性能。
掌握 useRef,就等于掌握了 React 中“非响应式可变值”的处理艺术。无论是初学者还是有经验的开发者,都应该熟练运用这个强大的 Hook。
下次遇到“需要保存一个值但不想重渲染”的需求时,请第一时间想到 useRef —— 它永远在那里,默默守护你的组件性能和逻辑清晰。