React Hook: useRef 详解

什么是 useRef

useRef 是一个 React Hook,它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。useRef 的主要特点是:

  1. 不会触发重新渲染:修改 ref.current 的值不会导致组件重新渲染
  2. 在组件重新渲染之间保持值:ref 对象在组件的整个生命周期中保持不变
  3. 可以访问 DOM 元素:通过 ref 可以直接操作 DOM

基本语法

const ref = useRef(initialValue);

// 访问值
ref.current

// 修改值
ref.current = newValue;

主要用途

1. 访问 DOM 元素

最常见的用法是获取 DOM 元素的引用,以便直接操作 DOM(如聚焦、滚动、测量尺寸等)。

import { useRef, useEffect } from 'react';

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

  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}

关键点:

  • 使用 ref={inputRef} 将 ref 附加到 JSX 元素
  • 通过 inputRef.current 访问实际的 DOM 元素
  • 使用可选链 ?. 避免 null 引用错误

2. 保存可变值(不触发重新渲染)

当你需要一个值在组件重新渲染之间保持不变,但又不想触发重新渲染时,可以使用 useRef

function RefCounter() {
  const countRef = useRef(0);
  const [renderCount, setRenderCount] = useState(0);

  const increment = () => {
    countRef.current += 1;
    console.log('当前计数:', countRef.current);
    // 注意:这不会触发重新渲染!
    // 如果需要显示变化,需要更新 state
    setRenderCount(prev => prev + 1);
  };

  return (
    <div>
      <p>Ref 计数: {countRef.current}</p>
      <button onClick={increment}>增加</button>
    </div>
  );
}

useState 的区别:

  • useState:修改值会触发重新渲染
  • useRef:修改值不会触发重新渲染

3. 保存前一个值

使用 useRefuseEffect 可以轻松保存前一个值:

function PreviousValue() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef<number>();

  useEffect(() => {
    // 在渲染后保存当前值作为"前一个值"
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>当前值: {count}</p>
      <p>前一个值: {prevCountRef.current ?? '无'}</p>
    </div>
  );
}

4. 保存定时器 ID 或其他需要清理的资源

function TimerComponent() {
  const [seconds, setSeconds] = useState(0);
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  const startTimer = () => {
    // 清除之前的定时器(如果存在)
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
    
    // 保存新的定时器 ID
    timerRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  };

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

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

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

为什么使用 ref 而不是 state?

  • 定时器 ID 不需要在 UI 中显示
  • 不需要因为定时器 ID 的变化而触发重新渲染
  • 只需要在需要清理时能够访问到它

useRef vs useState

特性useRefuseState
触发重新渲染❌ 否✅ 是
值在渲染间保持✅ 是✅ 是
可以访问 DOM✅ 是❌ 否
适合存储 UI 状态❌ 否✅ 是
适合存储不需要显示的值✅ 是❌ 否

常见使用场景

✅ 使用 useRef 的场景:

  1. 访问 DOM 元素

    • 聚焦输入框
    • 滚动到特定位置
    • 测量元素尺寸
    • 触发动画
  2. 保存不需要触发渲染的值

    • 定时器 ID
    • 订阅 ID
    • 前一个 props/state 的值
    • 缓存计算结果
  3. 存储可变对象引用

    • 避免在 useEffect 依赖数组中包含对象

❌ 不应该使用 useRef 的场景:

  1. 需要触发重新渲染的值 → 使用 useState
  2. 需要在 JSX 中显示的值 → 使用 useState
  3. 需要计算的值 → 使用 useMemouseState

完整示例:滚动到元素

import { useRef } from 'react';

function ScrollToElement() {
  const targetRef = useRef<HTMLDivElement>(null);

  const scrollToTarget = () => {
    targetRef.current?.scrollIntoView({ 
      behavior: 'smooth',
      block: 'center'
    });
  };

  return (
    <div>
      <button onClick={scrollToTarget}>滚动到目标</button>
      <div style={{ height: '1000px' }}>
        {/* 很多内容 */}
      </div>
      <div ref={targetRef} style={{ background: 'yellow' }}>
        <h2>目标元素</h2>
      </div>
    </div>
  );
}

注意事项

  1. 不要在渲染期间修改 ref.current

    // ❌ 错误:在渲染期间修改
    function BadComponent() {
      const ref = useRef(0);
      ref.current = 1; // 不要这样做!
      return <div>{ref.current}</div>;
    }
    
    // ✅ 正确:在事件处理或 useEffect 中修改
    function GoodComponent() {
      const ref = useRef(0);
      const handleClick = () => {
        ref.current = 1; // 可以
      };
      return <button onClick={handleClick}>点击</button>;
    }
    
  2. ref.current 可能是 null

    • 在组件首次渲染时,如果 ref 附加到 DOM 元素,ref.current 可能是 null
    • 使用可选链 ?. 来安全访问
  3. ref 不会自动清理

    • 如果 ref 存储的是需要清理的资源(如定时器、订阅),记得在 useEffect 的清理函数中清理

关键要点

  1. useRef 返回的对象在组件整个生命周期中保持不变
  2. 修改 ref.current 不会触发重新渲染
  3. 主要用于访问 DOM 和存储不需要触发渲染的值
  4. 使用 ref={myRef} 将 ref 附加到 JSX 元素
  5. 通过 myRef.current 访问值或 DOM 元素
  6. 总是使用可选链 ?. 来安全访问可能为 null 的 ref

实际应用示例

查看 App.tsx 文件中的以下组件来了解实际用法:

  • AutoFocusInput - 自动聚焦输入框
  • RefCounter - 使用 ref 存储计数器(不触发渲染)
  • PreviousValue - 保存前一个值
  • TimerComponent - 保存定时器 ID
  • ScrollToElement - 滚动到指定元素
Logo

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

更多推荐