React 中 useRef 不只是拿 DOM 还能存可变值

FreeGuideOnline 最新 2026-07-05

React 中 useRef 不只是拿 DOM 还能存可变值

许多 React 开发者初识 useRef 时,只把它当作获取 DOM 元素的快捷方式。但 useRef 的真正威力远不止于此——它可以在组件的整个生命周期内 持久化存储任意可变值,且改变其内容时不会触发组件重渲染。这篇教程将带你深入这一用法,从核心机制到实战技巧,让你彻底掌握 useRef 的进阶能力。

useRef 基础回顾

在 React 中,useRef 返回一个可变的 ref 对象,该对象仅有一个 .current 属性,你可以把任何值赋给它。

import { useRef } from 'react';

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

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

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </>
  );
}

这是最经典的用法:通过 ref 属性绑定到 DOM 元素,然后通过 ref.current 访问该原生节点。

useRefcreateRef 的关键区别在于:useRef 在函数组件每次渲染时返回的都是 同一个对象引用,而 createRef 每次渲染都会创建一个全新的 ref 对象。因此 useRef 能在多次渲染间保持数据不变,这为存储可变值打下了基础。

useRef 存储可变值的核心理念

useRef 存储的值发生改变时,不会触发组件重渲染。这与 useState 形成鲜明对比:修改 state 必然导致组件重新渲染,而修改 ref.current 则完全悄悄进行。

// 使用 state —— 每次点击都会渲染
const [count, setCount] = useState(0);
setCount(count + 1); // 触发渲染

// 使用 ref —— 点击后数值会变,但界面不刷新
const countRef = useRef(0);
countRef.current += 1; // 不触发渲染

这个特性让 useRef 成为存放那些 不影响视图输出、但需要在多次渲染间保持引用 的值的完美容器。比如:

  • 存储不需要驱动 UI 变化的数据(如定时器 ID、WebSocket 实例)
  • 记录某些状态的历史快照
  • 作为“实例变量”在回调中共享可变数据,绕过闭包陷阱

useRef 存储可变值的典型场景

保存定时器 ID 或任何外部实例

当使用 setIntervalsetTimeout 时,我们需要在组件卸载时清除它们。如果用 useState 存储 timer ID,每次赋值都会引起不必要的渲染,此时 useRef 是更优选择。

import { useRef, useEffect } from 'react';

function Timer() {
  const intervalRef = useRef(null);

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

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

  return <div>计时器运行中</div>;
}

其他如 WebSocket 连接、图表实例、第三方库对象等都可如此处理,避免将这些实例混入状态树。

保存前一个状态或属性(实现 usePrevious)

Hook 没有实例属性,useRef 可以用来手动实现类似“前一个状态”的功能。

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

// 在组件中使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>当前值: {count}</p>
      <p>上一次的值: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

注意这里 useEffect 会在渲染后更新 ref,所以 prevCount 总是显示“上一次渲染时的值”。这是一个非常经典的自定义 Hook。

记录组件是否已挂载(避免内存泄漏)

在异步操作(如网络请求)完成后,如果组件已经卸载,再去调用 setState 会导致 React 抛出警告。使用 useRef 存储一个 isMounted 标记可以优雅地解决这个问题。

import { useRef, useEffect, useState } from 'react';

function FetchData() {
  const [data, setData] = useState(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    // 组件挂载时设为 true
    isMountedRef.current = true;

    fetch('/api/data')
      .then(res => res.json())
      .then(data => {
        if (isMountedRef.current) {
          setData(data);
        }
      });

    // 组件卸载时设为 false
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}

随着 React 18 的并发特性普及,这种模式更推荐使用 AbortController 取消请求,但 isMounted 模式在仍有很多遗留场景中十分有用。

存储不需要参与渲染的计算结果缓存

有时我们需要在组件中缓存一些计算密集型的结果,但这些结果又不需要驱动 UI 更新。useRef 能充当一个简易的缓存容器。

function ExpensiveComponent({ data }) {
  const cacheRef = useRef({});

  const computeResult = (data) => {
    // 假设这是一个很耗时的计算
    return data.reduce((acc, cur) => acc + cur, 0);
  };

  const result = useMemo(() => {
    // 优先从缓存中读取
    if (cacheRef.current.data === data) {
      return cacheRef.current.result;
    }
    const res = computeResult(data);
    cacheRef.current = { data, result: res };
    return res;
  }, [data]);

  return <div>结果: {result}</div>;
}

虽然 useMemo 本身也可以缓存,但 useRef 给了我们手动控制缓存何时失效的能力,并且可以跨渲染保存更复杂的缓存结构。

在回调中访问最新值而无需依赖(结合 useCallback)

这是一个较少人知道但极其实用的技巧。当我们需要在 useCallback 包裹的回调中访问最新的 state 或 props,但又不想因为添加依赖而导致回调频繁重建时,可以借助 useRef 保存最新值。

function Chat({ userId }) {
  const [text, setText] = useState('');
  const textRef = useRef(text);

  // 让 ref 一直和最新的 text 同步
  useEffect(() => {
    textRef.current = text;
  });

  // 发送消息的回调永远引用同一个函数,但能通过 textRef 获取最新 text
  const sendMessage = useCallback(() => {
    const currentText = textRef.current;
    // 发送 currentText 到服务器
    console.log('发送:', currentText, '给用户', userId);
  }, [userId]); // 这里不需要 text 依赖

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={sendMessage}>发送</button>
    </div>
  );
}

这样做的好处是 sendMessage 不会因为 text 变化而重新创建,从而避免不必要的子组件重渲染(如果它作为 prop 传入)。这种“可变 ref”模式在性能优化中非常实用。

使用 useRef 存储可变值的注意事项

  1. 更新 ref 是同步的,但视图不会自动更新
    这意味着如果你偶尔想根据 ref 的值展示 UI,必须手动触发一次渲染(比如联合一个 useState 强制刷新),或者改用 state。

  2. 不要在渲染期间读写 ref
    遵循 React 规范,渲染函数应当是纯函数。读取写入 ref 建议在事件处理函数或 useEffect 中操作,以免出现不可预期的副作用。

  3. ref 的变更不会触发 useEffect 重新执行
    因为 React 不会追踪 ref 的变化,依赖数组中如果包含 ref,无法作为钩子的触发条件。

  4. 不要过度使用
    如果某些数据应该是响应式的,会直接影响 UI,请使用 useStateuseReduceruseRef 只适合那些“幕后”数据。

与 useMemo、useCallback 的配合

useRef 经常与这两个性能优化 Hook 协同工作:

  • useMemo 帮你缓存某个计算结果,而 useRef 可以帮你检查输入是否真的变化,从而跳过重复计算。
  • useCallback 帮你稳定函数引用,而搭配 useRef 能让回调在不添加依赖的情况下访问最新状态,打破闭包陷阱。

这样的组合在一些复杂组件(例如集成第三方库、需要稳定回调的拖拽或画布操作)中几乎是必备技能。

实战:构建一个带节流的搜索框

下面我们实现一个搜索框,要求:

  • 用户输入停止 500ms 后才发起搜索
  • 搜索过程中不阻塞用户继续输入
  • 避免不必要的 API 调用

我们将用到 useRef 存储定时器 ID,并结合 useStateuseEffect

import { useState, useEffect, useRef } from 'react';

function DebouncedSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const timerRef = useRef(null);

  useEffect(() => {
    // 清除上一次的定时器
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }

    // 设置新的定时器
    timerRef.current = setTimeout(() => {
      if (query.trim()) {
        fetch(`/api/search?q=${query}`)
          .then(res => res.json())
          .then(data => setResults(data));
      }
    }, 500);

    // 清理函数:组件卸载时清除定时器
    return () => {
      clearTimeout(timerRef.current);
    };
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

这里 timerRef 完美存储了定时器的 ID,避免了定时器 ID 被 state 管理而触发多余渲染,同时清理函数也可靠地处理了组件卸载时的资源释放。

总结

  • useRef 除了绑定 DOM,更重要的能力是 在组件的整个生命周期内存储任意可变值
  • 修改 ref.current 不会导致重渲染,适合存放与 UI 无关的数据。
  • 常见场景包括:保存定时器、实现 usePrevious、记录挂载状态、缓存计算结果、稳定回调中的数据引用等。
  • useState 正确分工:能用 state 解决的不要用 ref,需要“偷偷”存值且不影响渲染的场合用 ref。

掌握了这些模式后,你会发现 useRef 是解决许多 React 组件边界问题的利器,也是编写高性能、易维护 Hook 的基础。

常见问题

Q:我可以直接让 useEffect 依赖于 ref 吗?
A:不可以。React 不会追踪 ref.current 的变化,所以即使它变了,useEffect 也不会重新运行。如果你需要在 ref 变化时执行某些操作,通常需要搭配一个 useState 来控制触发。

Q:useRef 适合存储在线状态吗?比如用户是否在线?
A:如果这个状态不影响 UI,可以使用 ref。但通常在线状态会影响界面展示(如头像旁的小绿点),所以应该用 state。如果你只是想在事件处理中知道当前在线状态而不刷新 UI,那么 ref 更合适。

Q:useRef 和类组件中的实例属性(this.xxx)有什么区别?
A:概念上非常类似,都是在组件整个生命周期中保存可变数据且不触发渲染。useRef 是函数组件中的等价方案。

Q:使用 useRef 保存可变值有什么性能优势吗?
A:避免了不必要的渲染是其最大优势。此外,在一些需要稳定引用的场景(如传递给 useCallback 的回调),合理使用 ref 可以减少子组件的重渲染次数,从而提升整体性能。