React useRef 完全指南:操作 DOM 与持久化变量
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 原理深度解析]()
---