useRef 到底用来干什么?不止获取 DOM 元素

2 阅读3分钟

useRef 到底用来干什么?不止获取 DOM 元素

如果你刚接触 React Hooks,useRef 给你的第一印象大概率是: “用来拿 DOM 的” 。

比如:

const inputRef = useRef<HTMLInputElement>(null);

<input ref={inputRef} />

这没错,但这只是 useRef 的“表面工作”。

真正理解 useRef,要抓住一句话:

useRef 是 React 中“跨渲染周期的可变容器”。

这篇文章,我们就从“拿 DOM”开始,逐步拆开 useRef 的本质、常见误区和高级用法。


一、useRef 到底是什么?

1. 定义

useRef<T>(initialValue: T): { current: T }
  • 返回一个可变对象​ { current: T }
  • 在组件的整个生命周期内保持不变
  • 修改 ref.current 不会触发组件重新渲染

这是它和 useState 最大的区别。


二、最基础用法:获取 DOM 元素

1. 获取 DOM

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>聚焦</button>
    </>
  );
}

2. 常见 DOM 场景

  • 聚焦 / 选中文本
  • 测量 DOM 尺寸
  • 滚动控制
  • 集成第三方 DOM 库(如 ECharts、Monaco Editor)
const chartRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (chartRef.current) {
    const width = chartRef.current.clientWidth;
    // 初始化图表
  }
}, []);

✅ DOM 操作是 useRef 的“第一职业”,但不是唯一职业。


三、useRef vs useState:关键区别

这是面试和实战中最容易混淆的点。

特性useRefuseState
是否触发渲染❌ 不触发✅ 触发
值是否跨渲染✅ 是✅ 是
是否“React 可控”❌ 不可控✅ 可控
典型用途DOM、实例、缓存UI 状态

一个经典例子

function Counter() {
  const countRef = useRef(0);
  const [count, setCount] = useState(0);

  const incrementRef = () => {
    countRef.current++;
    console.log('ref:', countRef.current);
    // UI 不会更新
  };

  const incrementState = () => {
    setCount(c => c + 1);
    // UI 会更新
  };

  return (
    <>
      <p>state: {count}</p>
      <button onClick={incrementRef}>Ref +1</button>
      <button onClick={incrementState}>State +1</button>
    </>
  );
}

👉 结论:

  • 需要 UI 响应 → useState
  • 需要“悄悄存点东西” → useRef

四、useRef 的隐藏技能:跨渲染保存可变值

1. 保存上一次的 props / state

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

使用:

const prevCount = usePrevious(count);

这在做“前后对比”时非常有用,比如动画、日志、表单脏检查。


2. 保存定时器 / 订阅实例

function Timer() {
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    timerRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);

    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, []);

  return <div>Timer</div>;
}

✅ 用 useRef 保存“清理对象”,比 let timer 更安全。


3. 防止重复初始化(单例)

function useOnce(init: () => any) {
  const ref = useRef<{ value: any }>();

  if (!ref.current) {
    ref.current = { value: init() };
  }

  return ref.current.value;
}

适合:

  • 创建 SDK 实例
  • 初始化 Web Worker
  • 创建全局缓存对象

五、useRef 在闭包陷阱中的救场

React Hooks 的一个经典坑:闭包捕获旧值。

问题示例

function Chat() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 永远是 0
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>+</button>;
}

用 useRef 解决

function Chat() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(countRef.current); // ✅ 永远是最新值
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>+</button>;
}

✅ useRef 是“逃离闭包陷阱”的常用手段。


六、useRef + 自定义 Hook = 强力组合

1. useLatest:永远拿到最新值

function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}

2. useMountedRef:判断组件是否挂载

function useMountedRef() {
  const ref = useRef(false);

  useEffect(() => {
    ref.current = true;
    return () => {
      ref.current = false;
    };
  }, []);

  return ref;
}

常用于异步请求中防止内存泄漏:

if (mountedRef.current) {
  setData(res);
}

七、useRef 的常见误区

❌ 误区 1:用 useRef 代替 useState

// 错误:UI 不会更新
const countRef = useRef(0);

✅ 只要 UI 依赖这个值,就不能用 useRef。


❌ 误区 2:在渲染阶段修改 ref.current

// ❌ 不推荐
ref.current = 123;
return <div />;

✅ 修改 ref 应放在:

  • useEffect
  • 事件处理函数
  • 自定义 Hook 内部逻辑

❌ 误区 3:认为 ref 是响应式的

ref.current 变化 不会触发组件更新,这是设计如此,不是 bug。


八、一句话总结 useRef

useRef 是一个“在组件生命周期内存在、可变、不触发渲染的盒子”。

它适合:

  • ✅ 获取 DOM
  • ✅ 保存实例
  • ✅ 缓存跨渲染的值
  • ✅ 解决闭包问题
  • ✅ 控制副作用生命周期

不适合:

  • ❌ UI 状态
  • ❌ 需要响应式的数据

九、什么时候该用 useRef?

你可以问自己三个问题:

  1. 这个值是否需要跨渲染存在?
  2. 修改它是否需要触发 UI 更新?
  3. 我是否只是想“存点东西”,而不是“驱动 UI”?

如果答案是 “是 / 否 / 是” ​ → 用 useRef。