Skip to content

useRef 有哪些常见的应用场景?

useRef 是 React Hooks 中一个独特且功能强大的工具。与 useState 不同,更新 useRef 的值不会触发组件的重新渲染。这个核心特性决定了它的主要用途:在需要一个“可变容器”来跨渲染周期持久化数据,但又不希望这个数据的变化影响视图时,useRef 就成了最佳选择。

本质上,我们可以把 useRef 看作是函数组件实例上的一个“储物箱”,我们可以随时存取物品,而不用每次都重新整理(渲染)整个房间。

下面我们来探讨几个最常见的应用场景。

访问和操作 DOM 元素

这是 useRef 最广为人知的用途。在 React 的声明式世界里,我们通常不直接操作 DOM。但总有一些场景需要我们“破例”,例如:

  • 自动聚焦输入框

  • 获取元素的尺寸或位置

  • 控制视频或音频的播放

  • 集成需要直接访问 DOM 的第三方库(如 D3.js)

在这些情况下,useRef 提供了一个可靠的桥梁。

工作流程:

  1. 创建一个 ref 对象:const inputRef = useRef(null);

  2. 将其附加到 JSX 元素的 ref 属性上:<input ref={inputRef} />

  3. useEffect 或事件处理函数中,通过 inputRef.current 访问该 DOM 节点。

示例:页面加载后自动聚焦输入框

javascript
import React, { useRef, useEffect } from 'react';

function AutoFocusInput() {
  // 1. 创建一个 ref
  const inputRef = useRef(null);

  useEffect(() => {
    // 3. 在 effect 中访问 DOM 节点
    // inputRef.current 指向的就是真实的 <input> DOM 元素
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // 空依赖数组确保 effect 仅在组件挂载时运行一次

  return (
    <div>
      <p>输入框将在加载后自动获得焦点:</p>
      {/* 2. 将 ref 附加到 DOM 元素 */}
      <input ref={inputRef} type="text" placeholder="请在这里输入..." />
    </div>
  );
}

存储跨渲染周期的可变值

这是 useRef 更为精妙的用法。当我们有一个值,需要在多次渲染之间保持不变,但它的变化又不应该引起视图更新时,useRef 就非常适用。最典型的例子是管理定时器。

场景:管理 setTimeoutsetInterval

如果我们将定时器 ID 存储在 useState 中,每次 setTimerId 都会导致不必要的重渲染。而使用 useRef,我们可以悄无声息地在后台存储和清除定时器。

示例:一个简单的秒表

javascript
import React, { useState, useRef } from 'react';

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  
  // 使用 ref 来存储 interval ID
  const intervalRef = useRef(null);

  const handleStart = () => {
    // 防止重复启动
    if (intervalRef.current) return;

    intervalRef.current = setInterval(() => {
      // 注意:这里使用函数式更新,避免依赖外部的 `seconds` 变量
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);
  };

  const handleStop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null; // 清理 ref
    }
  };

  const handleReset = () => {
    handleStop();
    setSeconds(0);
  };

  return (
    <div>
      <h2>秒表: {seconds}s</h2>
      <button onClick={handleStart}>开始</button>
      <button onClick={handleStop}>停止</button>
      <button onClick={handleReset}>重置</button>
    </div>
  );
}

在这个例子中,intervalRef.current 的变化(从 null 变为一个数字 ID,再变回 null)完全独立于组件的渲染流程,实现了高效的状态管理。

缓存上一次的 Props 或 State

有时我们需要在渲染时拿到上一次的某个值,以便进行比较或计算。例如,我们想知道一个数字是增大了还是减小了。useRefuseEffect 的组合可以优雅地实现这个需求。

工作流程:

  1. useRef 创建一个 ref 来存储上一次的值。

  2. useEffect 中,先执行逻辑(此时 ref 中还是上一次的值),然后更新 ref 为当前值,为下一次渲染做准备。

示例:显示当前值和上一个值

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

function PreviousValueDisplay({ value }) {
  // 用 ref 存储上一次的 value
  const prevValueRef = useRef();

  useEffect(() => {
    // effect 会在每次渲染完成后运行
    // 所以在这次更新 ref 之前,它存储的还是上一次渲染的值
    prevValueRef.current = value;
  }, [value]); // 依赖 value,确保 value 变化时更新 ref

  // 在渲染时,prevValueRef.current 还是上一次的值
  const prevValue = prevValueRef.current;

  return (
    <p>当前值: {value}, 上一个值: {prevValue === undefined ? 'N/A' : prevValue}</p>
  );
}

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      <button onClick={() => setCount(c => c - 1)}>减少</button>
      <PreviousValueDisplay value={count} />
    </div>
  );
}

这个模式非常实用,可以看作是实现了一个自定义 Hook usePrevious 的基础。

useState vs useRef:如何选择?

为了更好地理解 useRef,我们可以建立一个清晰的选择标准:

特性useStateuseRef
触发重渲染是,setState 会触发组件重新渲染。否,修改 .current 属性不会触发重渲染。
主要用途管理组件的声明式状态,即那些需要反映在 UI 上的数据。存储不需要触发 UI 更新的数据,或用于命令式地访问 DOM。
值更新方式异步更新,通过 setState 函数。同步更新,直接修改 .current 属性。
何时使用当数据变化需要让用户看到时。当你需要一个“后台”变量,或者需要与 React 之外的世界(如 DOM)交互时。

总结

useRef 为我们提供了一个逃生舱,让我们可以在必要时跳出 React 纯粹的声明式数据流。它就像是函数组件的“实例属性”,为我们处理那些与渲染无关的副作用、状态和外部交互提供了强大的支持。

掌握 useRef 的这几个核心场景,可以帮助我们编写出更干净、更高效、也更灵活的 React 组件。

不知道说啥了很无语了