讲解
useRef 返回一个 { current: ... } 对象,它有两个互不相干的用途。用途一(最广为人知):引用 DOM 元素——<input ref={inputRef}>,挂载后 inputRef.current 就是真实节点,.focus()、.scrollIntoView()、接第三方 DOM 库都靠它。用途二(常被忽视):存「不需要触发渲染的可变值」——定时器 id、上一次的 props、WebSocket 实例。改 ref.current 不会触发重渲染,这正是它和 useState 的分界线:影响界面的用 state,只影响逻辑的用 ref。
为什么异步回调里要读 ref 而不是 state?因为 state 是每次渲染的快照:setTimeout 三秒后执行,里面读到的 count 是创建回调那次渲染的值。而 ref 对象是同一个,ref.current 永远是最新的——把「最新值」镜像到 ref 里,异步代码就能读到当下。常见模式:useEffect(() => { ref.current = value }) 每次渲染同步一次。
操作 DOM 的时机:挂载前 ref.current 是 null,所以 .focus() 这类调用放 useEffect 里。ref 回调写法 ref={(el) => ...} 更灵活,可以在列表里逐个收集节点。纪律:渲染期间不要读写 ref.current(渲染要纯净),ref 的读写属于事件回调和 effect。
示例
对比 state 快照 vs ref 最新值,看清异步回调为什么该读 ref(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 模拟「state 是快照,ref 是最新值」
function createComponent() {
const state = { count: 0 };
const ref = { current: 0 };
const pending = [];
return {
click() {
state.count++;
ref.current = state.count; // 镜像最新值
// 事件回调里安排一个 3 秒后的读取(闭包捕获的是这次渲染的 state 对象)
const snapshot = state.count;
pending.push(() => ({ fromState: snapshot, fromRef: ref.current }));
},
flush: () => pending.map((f) => f()),
};
}
const c = createComponent();
c.click(); // count = 1,安排读取
c.click(); // count = 2,安排读取
c.click(); // count = 3,安排读取
const results = c.flush(); // 三个定时器同时到点
assert.deepStrictEqual(results, [
{ fromState: 1, fromRef: 3 }, // state 是各自渲染的快照
{ fromState: 2, fromRef: 3 }, // ref 永远是最新值
{ fromState: 3, fromRef: 3 },
]);
// ref 存定时器 id:改它不触发渲染
const timerRef = { current: null };
timerRef.current = setInterval(() => {}, 1000);
assert.notStrictEqual(timerRef.current, null);
clearInterval(timerRef.current); // 卸载时清理
timerRef.current = null;
assert.strictEqual(timerRef.current, null);
console.log('三个回调读到: state =', results.map((r) => r.fromState).join('/'), ', ref =', results[0].fromRef);
console.log('定时器 id 存 ref,清理后归零,全程没触发渲染');
真实组件:
import { useEffect, useRef, useState } from 'react';
function SearchBox() {
const inputRef = useRef(null); // 用途一:DOM 引用
const [keyword, setKeyword] = useState('');
const latestKeyword = useRef(''); // 用途二:最新值镜像
useEffect(() => {
inputRef.current.focus(); // 挂载后聚焦
}, []);
useEffect(() => {
latestKeyword.current = keyword; // 每次渲染同步最新值
});
function handleSearchLater() {
setTimeout(() => {
console.log('实际搜索:', latestKeyword.current); // 3 秒后读到的是当下值
}, 3000);
}
return (
<>
<input ref={inputRef} value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={handleSearchLater}>3 秒后搜索</button>
</>
);
}
常见坑
- 用 state 存定时器 id:每次存都触发一次无意义渲染;ref 才是这类「逻辑值」的家。
- 渲染期间读写 ref.current:StrictMode 双渲染下行为不可预测;ref 操作放 effect 和事件回调。
- 挂载前访问 DOM:顶层代码里 ref.current 是 null,DOM 操作放 useEffect。
- 拿 ref 当 state 用:改了 ref 界面不更新,然后困惑半天——影响显示的值必须是 state。
- 滥用 DOM 操作:能用 state/props 声明式表达的(禁用、显示隐藏)别用 ref 命令式改 DOM。
小结
ref = 不触发渲染的可变盒子;DOM 引用和「最新值镜像」两大用途;读写放 effect/回调,不放渲染期。下一章解决 props 透传:useContext。