useRef

useRef 是一个 React Hook,它可以让你引用一个对渲染不需要的值。

const ref = useRef(initialValue)

参考

useRef(initialValue)

在组件顶层调用 useRef 来声明一个 ref.

import { useRef } from 'react';

function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
// ...

查看更多示例。

参数

  • initialValue:你希望 ref 对象的 current 属性初始时的值。它可以是任何类型的值。该参数在初次渲染之后会被忽略。

返回值

useRef 返回一个只有单个属性的对象:

  • current:初始时,它被设置为你传入的 initialValue。之后你可以把它设为别的值。如果你把 ref 对象作为 JSX 节点的 ref 属性传给 React,React 会设置它的 current 属性。

在后续渲染中,useRef 会返回同一个对象。

注意事项

  • 你可以修改 ref.current 属性。与 state 不同,它是可变的。不过,如果它持有一个用于渲染的对象(例如,你的一部分 state),那么你不应该修改那个对象。
  • 当你改变 ref.current 属性时,React 不会重新渲染你的组件。React 不知道你何时改变了它,因为 ref 只是一个普通的 JavaScript 对象。
  • 在渲染期间不要写入 或读取 ref.current,初始化除外。这样会让组件行为变得不可预测。
  • 在严格模式下,React 会为帮助你发现意外的副作用而 调用你的组件函数两次。这是仅开发环境下的行为,不会影响生产环境。每个 ref 对象都会创建两次,但其中一个版本会被丢弃。如果你的组件函数是纯函数(它本应如此),这不应该影响行为。

用法

用 ref 引用一个值

在组件顶层调用 useRef 来声明一个或多个 refs.

import { useRef } from 'react';

function Stopwatch() {
const intervalRef = useRef(0);
// ...

useRef 返回一个 ref 对象,它有一个单独的 current 属性,初始值设为你提供的 初始值

在后续渲染中,useRef 会返回同一个对象。你可以修改它的 current 属性来存储信息,并在之后读取。这可能会让你联想到 state,但二者有一个重要区别。

修改 ref 不会触发重新渲染。 这意味着 ref 非常适合存储那些不会影响组件视觉输出的信息。比如,如果你需要存储一个 interval ID 并在之后取回它,你可以把它放进 ref。要更新 ref 里的值,你需要手动修改它的 current 属性

function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}

之后,你可以从 ref 中读取这个 interval ID,以便调用 清除该计时器

function handleStopClick() {
const intervalId = intervalRef.current;
clearInterval(intervalId);
}

使用 ref,可以确保:

  • 你可以在重新渲染之间 存储信息(不同于普通变量,它们会在每次渲染时重置)。
  • 修改它 不会触发重新渲染(不同于 state 变量,它们会触发重新渲染)。
  • 信息是局部的,属于组件的每个副本(不同于外部变量,它们是共享的)。

修改 ref 不会触发重新渲染,所以 ref 不适合存储你想显示在屏幕上的信息。对此请改用 state。更多内容请阅读 useRefuseState 之间进行选择。

useRef 引用值的示例

Example 1 of 2:
点击计数器

这个组件使用 ref 来跟踪按钮被点击了多少次。请注意,这里使用 ref 而不是 state 是可以的,因为点击次数只会在事件处理函数中读取和写入。

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

如果你在 JSX 中显示 {ref.current},数字不会在点击时更新。这是因为设置 ref.current 不会触发重新渲染。用于渲染的信息应该改用 state。

Pitfall

在渲染期间不要写入 或读取 ref.current

React 期望你的组件主体 表现得像一个纯函数

  • 如果输入(propsstatecontext)相同,它应该返回完全相同的 JSX。
  • 以不同的顺序调用它,或传入不同的参数,不应该影响其他调用的结果。

渲染期间 读取或写入 ref 会破坏这些预期。

function MyComponent() {
// ...
// 🚩 不要在渲染期间写入 ref
myRef.current = 123;
// ...
// 🚩 不要在渲染期间读取 ref
return <h1>{myOtherRef.current}</h1>;
}

你可以改为在 事件处理函数或 Effects 中读取或写入 ref。

function MyComponent() {
// ...
useEffect(() => {
// ✅ 你可以在 effects 中读取或写入 ref
myRef.current = 123;
});
// ...
function handleClick() {
// ✅ 你可以在事件处理函数中读取或写入 ref
doSomething(myOtherRef.current);
}
// ...
}

如果你 必须 在渲染期间读取 或写入 某些内容,请改用 use state

当你违反这些规则时,你的组件可能仍然能工作,但我们为 React 添加的大多数更新颖的功能都会依赖这些预期。更多内容请阅读 保持组件纯净。


使用 ref 操作 DOM

使用 ref 来操作 DOM 是特别常见的。React 对此提供了内置支持。

首先,使用 null 作为 初始值 声明一个 ref 对象

import { useRef } from 'react';

function MyComponent() {
const inputRef = useRef(null);
// ...

然后把你的 ref 对象作为 ref 属性传给你想操作的 DOM 节点的 JSX:

// ...
return <input ref={inputRef} />;

在 React 创建 DOM 节点并将其显示到屏幕后,React 会把你的 ref 对象的 current 属性设置为那个 DOM 节点。现在你可以访问 <input> 的 DOM 节点,并调用像 focus() 这样的方:

function handleClick() {
inputRef.current.focus();
}

当节点从屏幕上移除时,React 会把 current 属性重新设为 null

更多内容请阅读 使用 refs 操作 DOM。

useRef 操作 DOM 的示例

Example 1 of 4:
聚焦文本输入框

在这个示例中,点击按钮会聚焦输入框:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}


避免重新创建 ref 内容

React 会只保存一次初始 ref 值,并在后续渲染中忽略它。

function Video() {
const playerRef = useRef(new VideoPlayer());
// ...

虽然 new VideoPlayer() 的结果只在初次渲染中使用,但你仍然会在每次渲染时调用这个函数。如果它创建的是开销很大的对象,这会很浪费。

要解决这个问题,你可以像下面这样初始化 ref:

function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...

通常来说,在渲染期间写入或读取 ref.current 是不允许的。不过,在这个例子中是可以的,因为结果始终相同,而且这个条件只会在初始化时执行,所以它是完全可预测的。

Deep Dive

在稍后初始化 useRef 时如何避免 null 检查

如果你使用类型检查器,并且不想总是检查 null,你可以尝试下面这种模式:

function Video() {
const playerRef = useRef(null);

function getPlayer() {
if (playerRef.current !== null) {
return playerRef.current;
}
const player = new VideoPlayer();
playerRef.current = player;
return player;
}

// ...

这里,playerRef 本身是可空的。不过,你应该能够让你的类型检查器相信,getPlayer() 不会返回 null。然后在事件处理函数中使用 getPlayer()


故障排除

我无法获取自定义组件的 ref

如果你尝试像这样将 ref 传递给自己的组件:

const inputRef = useRef(null);

return <MyInput ref={inputRef} />;

你可能会在控制台中看到一个错误:

Console
TypeError: 无法读取 null 的属性

默认情况下,自己的组件不会向外暴露其内部 DOM 节点的 refs。

要修复这个问题,请找到你想要获取 ref 的组件:

export default function MyInput({ value, onChange }) {
return (
<input
value={value}
onChange={onChange}
/>
);
}

然后将 ref 添加到组件接受的 props 列表中,并像这样把 ref 作为 prop 传递给相关的子 内置组件

function MyInput({ value, onChange, ref }) {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
};

export default MyInput;

这样父组件就可以获取到它的 ref 了。

阅读更多关于访问另一个组件的 DOM 节点。