React Refs 与 useRef — 通往 DOM 的“秘密后门” 🚪
Source: Dev.to
React refs – useRef:通往 DOM 的秘密后门
在 React 中,useRef 常常被误解为仅仅是“获取 DOM 元素的引用”。实际上,它是一个功能强大的工具,能够在组件的整个生命周期内保存任何可变值,而不会触发重新渲染。本文将深入探讨 useRef 的工作原理、常见用例以及一些不太为人知的技巧。
目录
useRef 基础
import { useRef } from "react";
function MyComponent() {
const inputRef = useRef(null);
const focusInput = () => {
// 直接访问底层 DOM 节点
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
- 创建引用:
useRef(initialValue)返回一个拥有current属性的可变对象。 - 初始值:如果你传入
null,current将在第一次渲染后被 React 自动填充(如上例所示)。 - 不触发渲染:对
current的修改不会导致组件重新渲染,这正是它与useState的根本区别。
为什么 useRef 不会导致重新渲染?
React 在每次渲染时都会比较 状态(state)和 属性(props)的变化来决定是否需要更新 UI。useRef 返回的对象在整个组件生命周期内保持 同一引用,即使 current 的值改变,React 仍然认为引用本身没有变化。因此,它不会把这次变化视为需要重新渲染的原因。
关键点:
useRef适合存放 副作用(例如计时器 ID、上一次渲染的值、外部库实例等),而不是用于直接驱动 UI。
常见场景示例
1. 访问 DOM 元素(最常见)
const divRef = useRef(null);
useEffect(() => {
console.log(divRef.current?.offsetHeight);
}, []);
2. 保存上一次的 props / state
function Counter({ count }) {
const prevCountRef = useRef(count);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<p>
当前: {count}, 上一次: {prevCountRef.current}
</p>
);
}
3. 保存定时器 ID
const timerRef = useRef();
const start = () => {
timerRef.current = setInterval(() => {
console.log("tick");
}, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
};
4. 与第三方库的实例交互
import Chart from "chart.js";
function ChartComponent() {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
chartRef.current = new Chart(canvasRef.current, { /* config */ });
return () => chartRef.current.destroy();
}, []);
return <canvas ref={canvasRef} />;
}
与 useState 的对比
| 特性 | useState | useRef |
|---|---|---|
| 触发重新渲染 | ✅ 会在值变化时重新渲染 | ❌ 不会触发渲染 |
| 可变性 | 不可变(必须返回新值) | 可变(直接修改 current) |
| 适用场景 | UI 状态(需要展示的值) | 保存副作用、DOM 引用、上一次值等 |
| 初始值 | 通过函数惰性初始化(可选) | 直接传入任意值 |
进阶技巧:保持上一次的值
有时我们需要在 渲染周期 中获取前一次的状态或属性,以便进行比较或动画。下面的自定义 Hook 展示了如何利用 useRef 完成这一需求:
import { useRef, useEffect } from "react";
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用示例
function Demo({ count }: { count: number }) {
const prev = usePrevious(count);
return (
<div>
<p>当前: {count}</p>
<p>上一次: {prev}</p>
</div>
);
}
- 工作原理:在每次
value变化后,useEffect会把最新值写入ref.current。下一次渲染时,ref.current仍然保存着上一次的值。
结论
useRef并非仅仅是 “获取 DOM”,它是 保持跨渲染的可变引用 的通用工具。- 当你需要 存储不影响 UI 的数据(计时器 ID、上一次的 props、第三方库实例等)时,优先考虑
useRef。 - 记住:修改
ref.current不会导致重新渲染,因此请确保 UI 依赖的状态仍然使用useState或useReducer。
掌握 useRef 的细微差别后,你将能够写出更高效、更易维护的 React 代码,并且在需要直接操作底层 DOM 时拥有可靠的后门。祝编码愉快!
曾经需要直接操作 React 中的 DOM 元素,却感觉 React 在阻碍你吗?
这正是 useRef 的用武之地。可以把它想象成一个秘密后门,让你能够直接访问真实的 DOM——而且不违反 React 的任何规则。
State vs. Ref — 两句版
State → 更改会触发重新渲染。你使用 setter 函数来更新它。
Ref → 更改是静默的。你直接变更它,React 甚至不会眨眼。
Refs 就像你给自己贴的便利贴。React 并不在乎你在上面写了什么。
创建 Ref
import { useRef } from "react";
function MyComponent() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}
发生了什么
useRef(null)创建一个对象:{ current: null }。- 该对象被传递给
<input>的ref属性。 - React 将
inputRef.current填充为该输入框的 实际 DOM 节点。
现在 inputRef.current 就是页面上真实的 <input> 元素。
实际案例:自动滚动到新内容
import { useRef, useEffect, useState } from "react";
function RecipeApp() {
const [recipe, setRecipe] = useState(null);
const recipeSectionRef = useRef(null);
async function fetchRecipe() {
const response = await getRecipeFromAI(); // pretend API call
setRecipe(response);
}
useEffect(() => {
if (recipe && recipeSectionRef.current) {
recipeSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [recipe]);
return (
<div>
<button onClick={fetchRecipe}>Get a Recipe</button>
{recipe && (
<section ref={recipeSectionRef}>
<h2>{recipe.title}</h2>
<p>{recipe.instructions}</p>
</section>
)}
</div>
);
}
正在发生的步骤
- 用户点击 Get a Recipe。
- API 返回数据 → 状态更新 → React 重新渲染。
- 带有 ref 的
<section>现在已存在于 DOM 中。 useEffect运行,检测到食谱已加载,并调用scrollIntoView()。- 浏览器平滑滚动到食谱部分。
No document.getElementById. No query selectors. Just a clean ref.
“但为什么不直接使用 ID?”
你可以这样做:
...
document.getElementById("recipe-section").scrollIntoView();
它能工作……直到它不工作。React 鼓励 可复用组件。渲染相同的组件两次会产生重复的 ID,这在 HTML 中是无效的,并且是错误的来源。
Refs 完全避免了这个问题,因为它们的作用域限定在每个组件实例上。两个实例 → 两个独立的 refs → 零冲突。
思维模型速查表
| State | Ref | |
|---|---|---|
| 是否触发重新渲染? | 是 | 否 |
| 如何更新 | Setter 函数 | 直接变异 (myRef.current = …) |
| 常见用途 | UI 数据 | DOM 访问、计时器、先前值 |
| 形状 | 任意你设置的值 | { current: value } |
三条快速记忆规则
-
Ref 只是盒子。
useRef(initialValue)返回{ current: initialValue }—— 一个只有一个叫current的层架的盒子。 -
自由变更。
与 state 不同,你可以直接赋值myRef.current = "whatever"而不会触发重新渲染。 -
ref属性是魔法——但仅限于原生元素。
<input ref={myRef}>会让 React 将该 DOM 节点填入myRef.current。
<MyComponent ref={myRef}>除非使用forwardRef,否则仅仅是传递一个普通的名为ref的属性。
TL;DR
useRef创建一个持久的可变容器:{ current: value }。- 更改
.current不会导致重新渲染。 - 通过
ref属性将其附加到 DOM 元素上,以直接访问该节点。 - 适用于滚动、聚焦输入框、测量元素,或在渲染之间存储值而不触发更新。
Refs 起初可能感觉怪异,但一旦“弄懂”它们,就会变得自然而然。只要需要直接操作 DOM 或在渲染之间保持稳定的可变值,就使用它们。