React Refs 与 useRef — 通往 DOM 的“秘密后门” 🚪

发布: (2026年2月16日 GMT+8 02:09)
10 分钟阅读
原文: Dev.to

Source: Dev.to

React refs – useRef:通往 DOM 的秘密后门

在 React 中,useRef 常常被误解为仅仅是“获取 DOM 元素的引用”。实际上,它是一个功能强大的工具,能够在组件的整个生命周期内保存任何可变值,而不会触发重新渲染。本文将深入探讨 useRef 的工作原理、常见用例以及一些不太为人知的技巧。

目录

  1. useRef 基础
  2. 为什么 useRef 不会导致重新渲染?
  3. 常见场景示例
  4. useState 的对比
  5. 进阶技巧:保持上一次的值
  6. 结论

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 属性的可变对象。
  • 初始值:如果你传入 nullcurrent 将在第一次渲染后被 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 的对比

特性useStateuseRef
触发重新渲染✅ 会在值变化时重新渲染❌ 不会触发渲染
可变性不可变(必须返回新值)可变(直接修改 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 依赖的状态仍然使用 useStateuseReducer

掌握 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} />;
}

发生了什么

  1. useRef(null) 创建一个对象:{ current: null }
  2. 该对象被传递给 <input>ref 属性。
  3. 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>
  );
}

正在发生的步骤

  1. 用户点击 Get a Recipe
  2. API 返回数据 → 状态更新 → React 重新渲染。
  3. 带有 ref 的 <section> 现在已存在于 DOM 中。
  4. useEffect 运行,检测到食谱已加载,并调用 scrollIntoView()
  5. 浏览器平滑滚动到食谱部分。

No document.getElementById. No query selectors. Just a clean ref.

“但为什么不直接使用 ID?”

你可以这样做:

...
document.getElementById("recipe-section").scrollIntoView();

它能工作……直到它不工作。React 鼓励 可复用组件。渲染相同的组件两次会产生重复的 ID,这在 HTML 中是无效的,并且是错误的来源。

Refs 完全避免了这个问题,因为它们的作用域限定在每个组件实例上。两个实例 → 两个独立的 refs → 零冲突。

思维模型速查表

StateRef
是否触发重新渲染?
如何更新Setter 函数直接变异 (myRef.current = …)
常见用途UI 数据DOM 访问、计时器、先前值
形状任意你设置的值{ current: value }

三条快速记忆规则

  1. Ref 只是盒子。
    useRef(initialValue) 返回 { current: initialValue } —— 一个只有一个叫 current 的层架的盒子。

  2. 自由变更。
    与 state 不同,你可以直接赋值 myRef.current = "whatever" 而不会触发重新渲染。

  3. ref 属性是魔法——但仅限于原生元素。
    <input ref={myRef}> 会让 React 将该 DOM 节点填入 myRef.current
    <MyComponent ref={myRef}> 除非使用 forwardRef,否则仅仅是传递一个普通的名为 ref 的属性。

TL;DR

  • useRef 创建一个持久的可变容器:{ current: value }
  • 更改 .current 不会导致重新渲染。
  • 通过 ref 属性将其附加到 DOM 元素上,以直接访问该节点。
  • 适用于滚动、聚焦输入框、测量元素,或在渲染之间存储值而不触发更新。

Refs 起初可能感觉怪异,但一旦“弄懂”它们,就会变得自然而然。只要需要直接操作 DOM 或在渲染之间保持稳定的可变值,就使用它们。

0 浏览
Back to Blog

相关文章

阅读更多 »