React useRef 完全指南:操作 DOM 与持久化变量

小飞兽 React 181 次阅读 2026-05-17

React useRef 完全指南:操作 DOM 与持久化变量

一、Introduction

useRef 是 React 中最容易被低估的 Hooks 之一。它有两个核心用途:第一,访问 DOM 元素的引用,让你可以精确操作原生 DOM API;第二,创建一个持久化的可变容器,在组件整个生命周期内保持同一个引用,修改它不会触发重新渲染。

很多开发者只知道 useRef 用于 focus()scrollIntoView(),却不知道它还能用于存储那些"不需要触发渲染但又必须持久化"的值,比如定时器 ID、上一轮的 props 值、累加器等。掌握 useRef 是从 React 初学者迈向中高级的关键一步。

---

二、useRef 基础语法

#### 2.1 基本用法

import React, { useRef } from 'react';

function TextInputFocus() {
  // 创建一个 ref 对象,初始值为 null
  // current 属性存放实际的 DOM 元素或值
  const inputRef = useRef(null);

  const handleFocus = () => {
    // 通过 inputRef.current 访问真实的 DOM 元素
    // 调用原生 DOM 方法
    inputRef.current.focus();
    inputRef.current.value = '初始填充内容';
  };

  const handleSelect = () => {
    // 选中文本
    inputRef.current.select();
  };

  return (
    <div>
      {/* 把 ref 赋值给 JSX 元素的 ref 属性,React 自动填充 current */}
      <input ref={inputRef} type="text" placeholder="会自动聚焦" />
      <div style={{ marginTop: 12 }}>
        <button onClick={handleFocus}>聚焦并填充</button>
        <button onClick={handleSelect}>全选</button>
      </div>
    </div>
  );
}

export default TextInputFocus;

#### 2.2 useRef 的可变存储功能

useRef 不仅能存 DOM,还能存任何可变值,且不会触发重新渲染:

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

function PreviousValueTracker() {
  const [count, setCount] = useState(0);
  // 创建一个 ref 用来存储"上一次"的 count 值
  const previousCountRef = useRef(null);

  useEffect(() => {
    // 每次 count 变化时,把旧值存入 ref
    previousCountRef.current = count;
  });

  return (
    <div>
      <p>当前值:{count}</p>
      <p>上一次的值:{previousCountRef.current}</p>
      {/* 通过直接修改 .current 来更新 ref,不触发渲染 */}
      <button onClick={() => setCount(c => c + 1)}>递增</button>
      <button onClick={() => setCount(c => Math.max(0, c - 1))}>递减</button>
    </div>
  );
}

#### 2.3 存储定时器 ID,实现可靠的清理

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

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  // 用 ref 存储定时器 ID,因为定时器 ID 不需要触发渲染
  const timerRef = useRef(null);

  const start = () => {
    if (timerRef.current) return; // 防止重复启动
    setIsRunning(true);
    timerRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };

  const stop = () => {
    // 通过 ref 访问定时器 ID 进行清理
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
    setIsRunning(false);
  };

  const reset = () => {
    stop();
    setTime(0);
  };

  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  };

  return (
    <div>
      <h2>{formatTime(time)}</h2>
      <button onClick={isRunning ? stop : start}>
        {isRunning ? '暂停' : '开始'}
      </button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

export default Stopwatch;

---

三、useRef 与 useState 的核心区别

| 特性 | useRef | useState |
|------|--------|----------|
| 变化时触发渲染 | ❌ 否 | ✅ 是 |
| 存储内容 | 任意可变值 | 状态值(触发 UI 更新) |
| 持久性 | 组件整个生命周期 | 状态变化触发渲染后仍保留 |
| 用途场景 | DOM 操作、定时器ID、上一次的值 | UI 必须反映的动态数据 |

一个关键认知:useRef 的 .current 属性是可以直接赋值修改的,就像操作一个普通的 JavaScript 对象。修改它不会让组件重新渲染,但组件重新渲染也不会重置它(除非你用条件渲染让组件真正卸载再挂载)。

---

四、运行效果说明

TextInputFocus:渲染后点击"聚焦并填充"按钮,输入框自动获得焦点并填充文字;点击"全选"则选中文本。这是操作原生 DOM 的典型场景。

PreviousValueTracker:每次点击递增/递减时,页面显示"当前值"和"上一次的值",后者总是比前者慢一拍(即显示上一次渲染时的 count 值)。

Stopwatch:点击"开始",计时器每秒更新数字;点击"暂停",定时器被彻底清理,timerRef.current 设为 null,防止内存泄漏;组件卸载时定时器也会被清理。

---

五、常见问题(FAQ)

#### Q1:useRef 的初始值一定要是 null 吗?

不一定,初始值可以根据需要设置任何值(数字、对象、数组等)。但对于 DOM ref,初始值通常是 null,因为在组件挂载前(初次渲染时),DOM 元素还不存在,所以用 null 作为占位。

// 初始值可以是任何类型
const mutableValueRef = useRef({ count: 0 });
const previousPropsRef = useRef(null);
const animationFrameRef = useRef(null);

#### Q2:修改 useRef 的 current 会触发重新渲染吗?

不会。 useRef 的设计目的就是存储那些不需要让 UI 知道的可变值。如果你需要 UI 随某个值变化而更新,那个值应该用 useState。常见错误是把定时器 ID 或计算结果存进 useState 导致每次更新都触发渲染——这些场景应该用 useRef。

#### Q3:useRef 在函数组件每次渲染时是同一个对象吗?

是的。 useRef 在组件的整个生命周期内返回同一个引用对象(类似 class 组件的实例变量)。每次渲染时,你拿到的 useRef() 返回的都是同一个 { current: ... } 对象,只是 current 的值可以变化。这意味着你可以在渲染函数中直接读写 inputRef.current,不会像 useState 那样有"闭包中值过时"的问题。

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React useCallback 与 useMemo 性能优化指南]()
  • [React 自定义 Hooks 实战:逻辑复用与模块化]()
  • [React 列表渲染与 keys 原理深度解析]()

---