使用 Ref 引用值:在不重新渲染的情况下记住信息
欢迎来到 React 的 “脱围机制(Escape Hatches)”。 在前面的学习中,你掌握了所有的标准声明式技术:组件、JSX、Props、State。大多数应用 99% 的界面逻辑都应该建立在这些核心之上。
但有时,你可能会遇到一些特殊的现实需求:你希望组件“记住”某些信息,但这些信息改变时,你绝不希望触发一次全新的重新渲染。
这时,你可以使用 React 提供的脱围工具之一:useRef Hook。
1. 为组件添加一个 Ref
使用 useRef,需要从 React 引入并传入初始值:
import { useRef } from 'react';
const ref = useRef(0);
useRef 会返回一个极其简单的普通 JavaScript 对象,它的形状长成这样:
{
current: 0 // 你传入的初始值
}
你可以随时通过 ref.current 读取它,也可以随时向 ref.current 赋新值。
核心差异:
- 当你修改一个 State 时,React 会重新渲染组件;
- 当你修改一个 Ref 的
current属性时,React 根本不会重新渲染组件!
你可以把 Ref 想象成组件的一个私密小口袋:你可以在里面存放任何东西,而且你在口袋里悄悄翻动东西时,绝对不会惊动屏幕上的任何像素。
2. Ref vs State:全方位对比
| 对比维度 | useRef(initialValue) | useState(initialValue) |
|---|---|---|
| 返回值 | 返回 { current: initialValue } 可变对象 | 返回 [value, setValue] 元组 |
| 修改方式 | 直接赋值:ref.current = 5 | 调用 Setter:setValue(5) |
| 修改是否触发渲染 | 不会触发任何重新渲染! | 会排队触发一次重新渲染! |
| 可变性 | 可变的(Mutable):可以直接在事件中读写 | 不可变的(Immutable):必须使用新状态替换旧状态 |
| 在渲染期间是否可用 | 绝不应该在渲染中读写 ref.current! | 可以随时在渲染中安全读取 state 快照 |
3. 经典实战:制作一个高精度秒表(Stopwatch)
秒表是说明 useRef 价值的最绝佳案例。
我们要制作一个秒表:点击“开始”,每隔 10 毫秒刷新一次时间;点击“停止”,计时器暂停。
为了驱动界面的时间跳动,我们需要 State:
startTime:开始时的时间戳;now:当前的时间戳。
但是,要让定时器停止,我们需要调用浏览器的 clearInterval(intervalId)。
这个 intervalId 应该存在哪里?
- 如果存在普通局部变量里,每次组件重新渲染时局部变量都会被销毁,停止按钮根本拿不到它;
- 如果存在
useState里,每次记下intervalId就会多余触发一次渲染,毫无必要; - 最好的选择是:保存在
useRef中!
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// 用 ref 保存定时器的 ID,它的改变不需要触发任何渲染!
const timerRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
// 启动定时器,每 10 毫秒更新一次当前时间
clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
// 停止定时器
clearInterval(timerRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>已走过时间: {secondsPassed.toFixed(3)} 秒</h1>
<button onClick={handleStart}>
开始计时
</button>
<button onClick={handleStop}>
停止计时
</button>
</>
);
}
4. 黄金铁律:不要在渲染过程中读写 ref.current!
React 期望你的组件主体(函数渲染过程)保持纯函数的纯净性。 如果你的组件在计算 JSX 的时候直接写:
// ❌ 严重违规!绝对不要在渲染计算中修改 ref!
function MyComponent() {
const ref = useRef(0);
ref.current = ref.current + 1; // 渲染时不纯!
return <h1>{ref.current}</h1>; // 渲染中读取 ref 会导致不可预测的渲染差异
}
正确规则:
- 只在事件处理函数(Event Handlers),或者稍后要学的 useEffect 中读写
ref.current! - 如果某个值需要直接用来决定 JSX 怎么渲染,请使用
useState。
总结
useRef返回一个在重新渲染之间持久保留的可变对象{ current: initialValue };- 更改
ref.current不会触发重新渲染; - 适合用来存储定时器 ID、DOM 元素引用或任何不直接参与 JSX 计算的幕后标识;
- 绝不在渲染计算期间直接读写
ref.current。