很多人以为 useRef 只用来拿 DOM,其实它更像一个“不会触发重渲染的隐藏抽屉”。今天我们通过 4 个场景,让你彻底搞懂 useRef 的能力:操作 DOM、存储定时器、记录前一次值、配合外部库。

🧠 useRef 是什么?

const ref = useRef(initialValue);
  • 返回一个 { current: initialValue } 对象。
  • .current 可读可写,改它不会触发组件重新渲染。
  • 每次渲染得到的是同一个 ref 对象。

🎯 场景一:操作 DOM 元素

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="自动聚焦" />;
}
  • 类似 document.querySelector,但更安全。
  • 适合做表单聚焦、滚动到元素、测量宽高。

🔁 场景二:保存定时器/外部实例

function Timer() {
  const timerRef = useRef(null);
  const [count, setCount] = useState(0);

  const start = () => {
    if (timerRef.current) return;
    timerRef.current = setInterval(() => setCount((c) => c + 1), 1000);
  };

  const stop = () => {
    clearInterval(timerRef.current);
    timerRef.current = null;
  };

  useEffect(() => stop, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}
  • timerRef 保存的是“跨渲染的变量”。
  • 不用担心 setInterval 的返回值在重渲染时丢失。

🕰️ 场景三:记录上一次的值

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function Profile({ name }) {
  const prevName = usePrevious(name);
  return (
    <p>
      当前:{name}(上一次:{prevName ?? '无'})
    </p>
  );
}
  • useRef 像一个“时间胶囊”,记录上一次渲染的值。
  • 不会导致重新渲染,非常适合做动画、统计等对比逻辑。

🤝 场景四:关联第三方库

function Chart() {
  const chartRef = useRef(null);

  useEffect(() => {
    if (!chartRef.current) {
      chartRef.current = echarts.init(document.getElementById('chart'));
    }
    chartRef.current.setOption({ /* 配置项 */ });

    return () => chartRef.current?.dispose();
  }, []);

  return <div id="chart" style={{ height: 300 }} />;
}
  • 保存外部库的实例(ECharts、Mapbox、播放器)。
  • 在卸载阶段释放资源,避免内存泄漏。

🧱 useRef vs useState

对比项 useRef useState
修改是否触发渲染
适合存什么 DOM 节点、定时器、外部实例、上一轮值 UI 状态、需要触发更新的数据
是否稳定 返回同一个 ref 对象 每次渲染返回新的 state 值

⚠️ 常见坑

坑点 现象 解决
ref.current 为空 DOM 尚未挂载 useEffect 中访问或加条件判断
使用 ref.current 保存状态 UI 不更新 把需要展示的数据存在 state
父子组件传 ref 默认不生效 需要 forwardRef + useImperativeHandle

🧠 forwardRef & useImperativeHandle 示例

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    reset: () => (inputRef.current.value = ''),
  }));
  return <input ref={inputRef} {...props} />;
});

function App() {
  const ref = useRef();
  return (
    <div>
      <FancyInput ref={ref} />
      <button onClick={() => ref.current?.focus()}>聚焦</button>
      <button onClick={() => ref.current?.reset()}>清空</button>
    </div>
  );
}
  • 让父组件获取子组件的“命令式方法”。

🏁 小练习

  1. 做一个“秒表”组件,用 useRef 保存 start 时间,秒级更新。
  2. 写一个带撤销功能的输入框,把历史值放在 ref 的栈里。

下一篇我们继续扩展“性能优化工具包”——用 React.memoProfiler 掌控组件渲染频率,避免“全家桶一起重渲染”的惨剧。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐