
一句话:
useRef返回一个「在整个组件生命周期内保持同一引用」的对象,修改它的.current不会触发重渲染——因此它适合存「DOM 引用」与「与 UI 无关的可变值」。
一、返回值与三个特性
const ref = useRef(initialValue);
// ref: { current: T }
| 特性 | 说明 |
|---|---|
| 引用稳定 | 每次渲染返回同一个对象(ref === ref) |
| 修改不触发渲染 | 改 ref.current 不会让组件重新渲染 |
| 每次渲染读到最新值 | 因为它是同一个对象,任何时刻读都是最新写入的值 |
function Demo() {
const ref = useRef(0);
const [count, setCount] = useState(0);
console.log('渲染时 ref.current =', ref.current); // ⭐ 值会「累积」,因为对象不变
const handleClick = () => {
ref.current += 1; // 不触发渲染
setCount((c) => c + 1); // 触发渲染
};
return (
<>
count: {count}
ref: {ref.current} {/* 只在重渲染时才显示新值 */}
>
);
}
二、三种典型用途
1) 访问 DOM
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return ;
2) 保存「与 UI 无关的可变值」
// ① 定时器 / 订阅 ID
const timerRef = useRef();
useEffect(() => {
timerRef.current = window.setInterval(tick, 1000);
return () => clearInterval(timerRef.current);
}, []);
// ② 标志位
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
doSomethingOnUpdate();
}, [dep]);
// ③ 计数器(不需要展示)
const renderCount = useRef(0);
renderCount.current += 1;
// ④ 保存上一次的值
function usePrevious(value: T): T | undefined {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// ⑤ 保存「最新的 props / state」(解决闭包陷阱)
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value; // 每次渲染更新(不触发渲染)
});
// 在「只在挂载时建立」的回调里读 latestValue.current 就能拿到最新值
3) 保存「不需要触发渲染」的第三方实例
const chartRef = useRef(null);
useEffect(() => {
chartRef.current = echarts.init(elRef.current!);
return () => chartRef.current?.dispose();
}, []);
const update = () => {
chartRef.current?.setOption(newOption); // 直接操作实例,不经过 state
};
三、useRef vs useState
| 维度 | useRef |
useState |
|---|---|---|
| 触发重渲染 | ❌ | ✅ |
| 渲染中读取 | ⚠️ 不应用于决定渲染结果 | ✅ 值随渲染更新 |
| 值的稳定性 | 对象引用不变,值可变 | 每次变化产生新值 |
| 适合存 | DOM 引用、定时器 ID、标志位、上一次的值 | 影响 UI 的数据 |
与 memo 的关系 |
不参与 props | 变化会导致下游更新 |
| 更新时机 | 立即生效 | 下次渲染才生效 |
// 关键差异演示:需要在「当前这一帧」读到的值用 ref
function Comp() {
const [a, setA] = useState(0);
const b = useRef(0);
const handleClick = () => {
setA(a + 1); // a 的值要等下次渲染才变
b.current += 1; // b.current 立即变化
console.log(a, b.current); // 0, 1 (a 还是旧值)
};
}
选择标准:
这个值「变化时需要更新 UI」吗?
├─ 是 → useState
└─ 否 → useRef
(DOM 引用、定时器、标志位、计数器、第三方实例、最新值缓存)
四、useRef vs 模块级变量
// ❌ 模块级变量:所有组件实例共享同一个值
let count = 0;
function Comp() {
count++; // 三个实例会互相干扰
return {count};
}
// ✅ useRef:每个组件实例有自己独立的值
function Comp2() {
const count = useRef(0);
count.current++; // 实例之间互不影响
return {count.current};
}
| 维度 | 模块级变量 | useRef |
|---|---|---|
| 作用域 | 整个模块(所有实例共享) | 每个组件实例独立 |
| 与组件生命周期 | 无关(永不重置) | 实例销毁即释放 |
| 适合 | 真正的全局常量/缓存 | 组件实例级别的可变值 |
五、初始值的「惰性」
// ⚠️ useRef 的初始值也会「每次都求值」,但只有第一次的结果被使用
const ref = useRef(expensiveCompute()); // 每次渲染都执行 expensiveCompute
// ✅ 需要惰性时自己包一层
const ref2 = useRef(null);
if (ref2.current === null) {
ref2.current = expensiveCompute(); // 只在首次渲染执行
}
对比:
① useState(() => v) → 官方支持惰性初始化(传函数)
② useRef(v) → ❌ 不支持惰性初始化,v 每次渲染都会被求值
所以「useRef 存一个需要昂贵计算的值」时要手动判断 null,
这也是「延迟初始化 ref」的常见写法:
const ref = useRef(null);
if (!ref.current) ref.current = createExpensiveThing();
六、常见模式合集
// ① 只在挂载/卸载时执行,但需要读最新值
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
// 每次渲染同步最新回调(不重建定时器)
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]); // ⭐ 依赖只有 delay,不因 callback 变化重建
}
// ② 只在挂载时执行一次(规避 StrictMode 的干扰时用 ref 做幂等)
const initialized = useRef(false);
useEffect(() => {
if (initialized.current) return;
initialized.current = true;
initThirdPartyLibrary(); // ⚠️ 注意:这只是「规避」,不是治本
}, []);
// ③ 记录滚动位置
const scrollPos = useRef>({});
const saveScroll = (key: string, top: number) => { scrollPos.current[key] = top; };
const restoreScroll = (key: string) => (el: HTMLElement | null) => {
if (el) el.scrollTop = scrollPos.current[key] ?? 0;
};
// ④ 保存「最新的请求结果」用于竞态比较
const requestSeq = useRef(0);
async function search(keyword: string) {
const seq = ++requestSeq.current;
const res = await api.search(keyword);
if (seq !== requestSeq.current) return; // ⭐ 过期响应直接丢弃
setResults(res);
}
// ⑤ 暴露「可取消」的操作
const controllerRef = useRef();
const cancel = () => controllerRef.current?.abort();
七、常见坑
// ① 在渲染中读写 ref 影响渲染结果
function Comp() {
const ref = useRef(0);
ref.current++; // ❌ 渲染不纯(StrictMode 下会翻倍)
return {ref.current};
}
// ② 用 ref 存「需要驱动 UI 的值」
const [text, setText] = ...; // ✅ 应该用 useState
const textRef = useRef(''); // ❌ UI 不会更新
// ③ 在首次渲染时读 ref.current 期望有值
useEffect(() => {
console.log(ref.current); // ✅ 这里是 DOM
}, []);
// 但在组件函数体里(渲染阶段)是 null
// ④ 忘记判空
ref.current.focus(); // ❌ 可能 null
ref.current?.focus(); // ✅
// ⑤ useRef 的初始值做昂贵计算
useRef(buildBigData()); // ❌ 每次渲染都执行
// ✅ 用「if (!ref.current)」延迟初始化
// ⑥ 想「读取最新 props 但不重建订阅」却用了依赖数组
// → 需要用「savedCallback / latestValue」的 ref 模式
// ⑦ 用 ref 在父子组件间传数据
// ⚠️ 反模式(应该用 props/Context)
// ⑧ 认为 useRef 变化会触发渲染
setCount(count + 1); // 渲染
ref.current++; // ❌ UI 不会变
// ⑨ 用 useRef 替代「应该提升的状态」
// → 多个组件需要共享的值应该用 state/Context,而不是 ref
// ⑩ 在 StrictMode 里用 ref 标志位「绕过」双调用而不修问题
// → 掩盖了真正的问题(见 8.5)
面试延伸
- 「
useRef和useState的区别?」
核心差异是「改变是否触发重渲染」:useState的值变化会触发组件重新渲染,且每次渲染拿到的是新的快照值;useRef返回的对象在渲染之间保持同一引用,修改.current不触发渲染,并且立即生效(不需要等下次渲染)。所以useRef适合存「与 UI 无关的可变值」,useState适合存「影响 UI 的数据」。 -
「为什么用
useRef存定时器/订阅 ID?」
因为定时器 ID 是「与 UI 无关的可变值」:它需要在多次渲染之间保持,但它的变化不应该触发重渲染。如果存在useState里,每次设置 ID 都会多一次渲染(而且可能陷入「渲染 → 设置 → 渲染」的循环)。所以「定时器 ID、AbortController、第三方实例、标志位、计数器」都应该放useRef。 -
「
ref.current什么时候能拿到 DOM?」
在组件函数执行期间(渲染阶段)它是上一轮的值或null(首次渲染是null);在 commit 阶段之后(即useEffect/useLayoutEffect里)才是当前渲染对应的真实 DOM;组件卸载后会被置回null。所以不要在渲染函数里读它,也不要在清理函数里依赖它做 DOM 操作。 -
「
useRef能解决闭包陷阱吗?」
能,这是它最常见的用途之一:用一个 ref 保存「最新值」(每次渲染都更新ref.current,这个更新不触发渲染,所以也不会造成循环),然后在「只在挂载时建立」的回调(如setInterval、订阅、事件监听)里读ref.current—— 就能拿到最新值。这是useInterval/useEventListener这类 Hook 的标准写法。 -
「
useRef的初始值会每次渲染都求值吗?」
会。useRef(expensiveCompute())每次渲染都会执行expensiveCompute()(只是从第二次开始结果被忽略)。所以需要惰性时要用「延迟初始化」的写法:const ref = useRef(null); if (ref.current === null) ref.current = expensiveCompute();注意这与
useState不同——useState支持传函数实现惰性初始化,useRef不支持。 -
「
useRef和模块级变量有什么区别?」
模块级变量是整个模块共享的(同一个模块里所有组件实例读写同一份数据),且永不重置;useRef是每个组件实例独立的,随实例销毁而释放。所以「组件实例级别的可变值」必须用useRef;模块级变量只适合「真正的全局常量或缓存」(如全局配置、Map缓存)。 -
「
useRef能用来在组件间传数据吗?」
技术上可以(把 ref 作为 props 传下去),但不应该——因为:① 修改 ref 不触发渲染,接收方无法感知变化(除非恰好有别的更新);② 破坏单向数据流(数据来源难追踪);③ 难以测试与调试。组件间的数据传递应该用 props、回调或 Context;ref 只用于「DOM 访问」与「与 UI 无关的可变值」以及useImperativeHandle暴露的命令式接口。 -
「
useRef会不会导致内存泄漏?」
它本身不会(ref随组件实例一起被回收),但如果ref.current里存的资源需要手动释放(定时器、订阅、WebSocket、第三方实例),忘记释放就会泄漏——这与useRef无关,只是「资源管理」的问题。所以规范是:凡是存在ref里的资源,都要在清理函数里释放(clearInterval/abort/close/destroy/disconnect)。
一句话速记
useRef返回跨渲染保持同一引用的对象,改.current不触发渲染且立即生效;它存三类东西——DOM 引用、与 UI 无关的可变值(定时器/标志位/计数器/最新值)、第三方实例;useState变化触发渲染、useRef不触发(判断标准是「变化要不要更新 UI」);初始值每次都求值(要惰性就自己判null);它与模块级变量的区别是「每个实例独立」;别用 ref 传数据(那是 props 的活)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!