React Hook 之 useRef详解

589 阅读3分钟

useRef 详解:功能、用法与常见场景

在 React 中,useRef 是一个非常强大且多功能的 Hook,它主要用于:

  1. 保存可变值:在组件生命周期内保持引用不变
  2. 访问 DOM 元素:替代传统的 ref 属性
  3. 缓存复杂对象:避免重复计算或渲染

下面从原理到实践,详细解析 useRef 的各种用法。

一、核心概念与基本用法

1. 基本语法

const refContainer = useRef(initialValue);
  • initialValue:初始值,可以是任意类型
  • refContainer:返回的 ref 对象,包含一个可变的 .current 属性

2. 关键特性

  • 引用不变性:组件重新渲染时,useRef 返回的始终是同一个 ref 对象
  • 可变值存储ref.current 可以随时修改,不会触发重新渲染
  • 跨渲染周期共享ref.current 的值在组件整个生命周期内保持

二、常见应用场景

1. 访问 DOM 元素

最常见的用法是获取 DOM 节点,执行聚焦、测量尺寸等操作。

示例:聚焦输入框

import { useRef } from 'react';

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

  const handleClick = () => {
    inputRef.current.focus(); // 直接访问 DOM 方法
  };

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

示例:测量元素尺寸

function MeasureElement() {
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  const measure = () => {
    if (divRef.current) {
      const { width, height } = divRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  };

  return (
    <div>
      <div ref={divRef} style={{ width: '50%', margin: 'auto' }}>
        测量我
      </div>
      <button onClick={measure}>获取尺寸</button>
      <p>宽: {dimensions.width}px, 高: {dimensions.height}px</p>
    </div>
  );
}

2. 存储定时器或异步操作

保存定时器 ID 或其他需要清理的资源,避免内存泄漏。

示例:使用 ref 保存定时器

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

  const startTimer = () => {
    timerRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(timerRef.current); // 安全清除定时器
  };

  useEffect(() => {
    return () => clearInterval(timerRef.current); // 组件卸载时清理
  }, []);

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

3. 缓存复杂对象

避免在每次渲染时重新创建相同的对象,尤其是大型数据结构或函数。

示例:缓存大型数组

function HeavyComponent() {
  const largeArrayRef = useRef(null);

  // 只在首次渲染时创建大型数组
  if (!largeArrayRef.current) {
    largeArrayRef.current = new Array(1000).fill(0).map((_, i) => i);
  }

  return (
    <div>
      {/* 使用 largeArrayRef.current */}
    </div>
  );
}

4. 存储上一次的值

在状态更新时保留之前的值,用于比较或计算差值。

示例:比较当前值与上一次的值

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

  useEffect(() => {
    prevCountRef.current = count; // 保存当前值到 ref
  }, [count]);

  const prevCount = prevCountRef.current; // 获取上一次的值

  return (
    <div>
      <p>现在: {count}, 之前: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

5. 跨组件通信

在某些场景下,可以通过父组件传递 ref 实现跨层级通信。

示例:父组件调用子组件方法

// 子组件
function ChildComponent() {
  const doSomething = () => {
    console.log('子组件执行操作');
  };

  // 将方法挂载到 ref 上
  const ref = useRef({ doSomething });

  return <div ref={ref}>子组件</div>;
}

// 父组件
function ParentComponent() {
  const childRef = useRef(null);

  const handleClick = () => {
    childRef.current?.doSomething(); // 调用子组件方法
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>调用子组件方法</button>
    </div>
  );
}

三、useRef vs useState

特性useRefuseState
触发重新渲染
值的持续性整个组件生命周期每次渲染可能不同(基于依赖)
用途DOM 访问、存储临时值管理驱动 UI 渲染的状态
更新方式直接修改:ref.current = value通过 setter:setState(value)

四、注意事项与常见误区

  1. 不要在渲染过程中修改 ref.current

    • 可能导致意外行为,因为渲染期间的修改不会反映到当前渲染中。
  2. 避免过度使用 useRef

    • 如果某个值需要触发 UI 更新,应该使用 useStateuseReducer
  3. 清理副作用

    • 如果 ref.current 保存了需要清理的资源(如定时器),在 useEffect 中进行清理。
  4. 与 forwardRef 结合使用

    • 当需要将 ref 传递给子组件时,使用 React.forwardRef
    const Child = React.forwardRef((props, ref) => {
      return <input ref={ref} />;
    });
    

五、总结

useRef 是 React 中一个多功能的 Hook,核心价值在于:

  1. 保持引用稳定性:避免因引用变化导致不必要的重渲染
  2. 存储可变数据:在不触发渲染的情况下保存和访问值
  3. DOM 操作:安全高效地访问和操作 DOM 元素

合理使用 useRef 可以解决许多性能问题和复杂场景,但需注意与 useState 的边界,确保状态管理逻辑清晰。