陌上人如玉
公子世无双

2.5 useRef

2.5 useRef

一句话: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)

面试延伸

  1. 「useRef 和 useState 的区别?」
    核心差异是「改变是否触发重渲染」:useState 的值变化会触发组件重新渲染,且每次渲染拿到的是新的快照值;useRef 返回的对象在渲染之间保持同一引用,修改 .current 不触发渲染,并且立即生效(不需要等下次渲染)。所以 useRef 适合存「与 UI 无关的可变值」,useState 适合存「影响 UI 的数据」。

  2. 「为什么用 useRef 存定时器/订阅 ID?」
    因为定时器 ID 是「与 UI 无关的可变值」:它需要在多次渲染之间保持,但它的变化不应该触发重渲染。如果存在 useState 里,每次设置 ID 都会多一次渲染(而且可能陷入「渲染 → 设置 → 渲染」的循环)。所以「定时器 ID、AbortController、第三方实例、标志位、计数器」都应该放 useRef。

  3. 「ref.current 什么时候能拿到 DOM?」
    在组件函数执行期间(渲染阶段)它是上一轮的值或 null(首次渲染是 null);在 commit 阶段之后(即 useEffect / useLayoutEffect 里)才是当前渲染对应的真实 DOM;组件卸载后会被置回 null。所以不要在渲染函数里读它,也不要在清理函数里依赖它做 DOM 操作。

  4. 「useRef 能解决闭包陷阱吗?」
    能,这是它最常见的用途之一:用一个 ref 保存「最新值」(每次渲染都更新 ref.current,这个更新不触发渲染,所以也不会造成循环),然后在「只在挂载时建立」的回调(如 setInterval、订阅、事件监听)里读 ref.current —— 就能拿到最新值。这是 useInterval / useEventListener 这类 Hook 的标准写法。

  5. 「useRef 的初始值会每次渲染都求值吗?」
    会。useRef(expensiveCompute()) 每次渲染都会执行 expensiveCompute()(只是从第二次开始结果被忽略)。所以需要惰性时要用「延迟初始化」的写法:

    const ref = useRef(null);
    if (ref.current === null) ref.current = expensiveCompute();
    

    注意这与 useState 不同——useState 支持传函数实现惰性初始化,useRef 不支持。

  6. 「useRef 和模块级变量有什么区别?」
    模块级变量是整个模块共享的(同一个模块里所有组件实例读写同一份数据),且永不重置;useRef 是每个组件实例独立的,随实例销毁而释放。所以「组件实例级别的可变值」必须用 useRef;模块级变量只适合「真正的全局常量或缓存」(如全局配置、Map 缓存)。

  7. 「useRef 能用来在组件间传数据吗?」
    技术上可以(把 ref 作为 props 传下去),但不应该——因为:① 修改 ref 不触发渲染,接收方无法感知变化(除非恰好有别的更新);② 破坏单向数据流(数据来源难追踪);③ 难以测试与调试。组件间的数据传递应该用 props、回调或 Context;ref 只用于「DOM 访问」与「与 UI 无关的可变值」以及 useImperativeHandle 暴露的命令式接口。

  8. 「useRef 会不会导致内存泄漏?」
    它本身不会(ref 随组件实例一起被回收),但如果 ref.current 里存的资源需要手动释放(定时器、订阅、WebSocket、第三方实例),忘记释放就会泄漏——这与 useRef 无关,只是「资源管理」的问题。所以规范是:凡是存在 ref 里的资源,都要在清理函数里释放(clearInterval / abort / close / destroy / disconnect)。

一句话速记

useRef 返回跨渲染保持同一引用的对象,改 .current 不触发渲染且立即生效;它存三类东西——DOM 引用、与 UI 无关的可变值(定时器/标志位/计数器/最新值)、第三方实例;useState 变化触发渲染、useRef 不触发(判断标准是「变化要不要更新 UI」);初始值每次都求值(要惰性就自己判 null);它与模块级变量的区别是「每个实例独立」;别用 ref 传数据(那是 props 的活)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 2.5 useRef

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫