使用引用引用值

当你希望一个组件“记住”某些信息,但又不希望这些信息触发新的渲染时,你可以使用ref

🌐 When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.

你将学习到

  • 如何向组件添加引用
  • 如何更新引用的值
  • 引用与状态有何不同
  • 如何安全地使用引用

向你的组件添加引用

🌐 Adding a ref to your component

你可以通过从 React 导入 useRef Hook 来为你的组件添加一个 ref:

🌐 You can add a ref to your component by importing the useRef Hook from React:

import { useRef } from 'react';

在你的组件内部,调用 useRef Hook,并将你想要引用的初始值作为唯一参数传入。例如,这里有一个对值 0 的引用:

🌐 Inside your component, call the useRef Hook and pass the initial value that you want to reference as the only argument. For example, here is a ref to the value 0:

const ref = useRef(0);

useRef 返回一个像这样的对象:

{
current: 0 // The value you passed to useRef
}
An arrow with 'current' written on it stuffed into a pocket with 'ref' written on it.

Illustrated by Rachel Lee Nabors

你可以通过 ref.current 属性访问该 ref 的当前值。这个值是故意可变的,这意味着你既可以读取它,也可以修改它。这就像是你的组件中的一个秘密口袋,React 并不会追踪它。(这就是它成为 React 单向数据流“逃生通道”的原因——下面会详细说明!)

🌐 You can access the current value of that ref through the ref.current property. This value is intentionally mutable, meaning you can both read and write to it. It’s like a secret pocket of your component that React doesn’t track. (This is what makes it an “escape hatch” from React’s one-way data flow—more on that below!)

这里,每次点击按钮都会增加 ref.current

🌐 Here, a button will increment ref.current on every click:

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>
  );
}

ref 指向一个数字,但像 state 一样,你可以指向任何东西:一个字符串、一个对象,甚至一个函数。与 state 不同,ref 是一个普通的 JavaScript 对象,具有你可以读取和修改的 current 属性。

🌐 The ref points to a number, but, like state, you could point to anything: a string, an object, or even a function. Unlike state, ref is a plain JavaScript object with the current property that you can read and modify.

请注意,组件不会在每次递增时重新渲染。 像状态一样,refs 会在多次渲染之间被 React 保留。然而,设置状态会重新渲染组件。更改 ref 不会!

🌐 Note that the component doesn’t re-render with every increment. Like state, refs are retained by React between re-renders. However, setting state re-renders a component. Changing a ref does not!

示例:制作一个秒表

🌐 Example: building a stopwatch

你可以在单个组件中结合使用 refs 和 state。例如,我们来做一个秒表,用户可以通过按下按钮来启动或停止。为了显示自用户按下“开始”按钮以来经过了多少时间,你需要记录下开始按钮被按下的时间以及当前时间。这些信息用于渲染,因此你需要将它们保存在 state 中:

🌐 You can combine refs and state in a single component. For example, let’s make a stopwatch that the user can start or stop by pressing a button. In order to display how much time has passed since the user pressed “Start”, you will need to keep track of when the Start button was pressed and what the current time is. This information is used for rendering, so you’ll keep it in state:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

当用户按下“开始”时,你将使用setInterval来每10毫秒更新一次时间:

🌐 When the user presses “Start”, you’ll use setInterval in order to update the time every 10 milliseconds:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Start counting.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Update the current time every 10ms.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

当按下“停止”按钮时,你需要取消现有的间隔,以便它停止更新 now 状态变量。你可以通过调用 clearInterval 来实现,但你需要传入先前用户按下开始时由 setInterval 调用返回的间隔 ID。你需要将间隔 ID 存放在某个地方。由于间隔 ID 不用于渲染,你可以将它保存在 ref 中:

🌐 When the “Stop” button is pressed, you need to cancel the existing interval so that it stops updating the now state variable. You can do this by calling clearInterval, but you need to give it the interval ID that was previously returned by the setInterval call when the user pressed Start. You need to keep the interval ID somewhere. Since the interval ID is not used for rendering, you can keep it in a ref:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

当一条信息用于渲染时,将其保存在状态中。当一条信息只被事件处理程序使用,并且更改它不需要重新渲染时,使用 ref 可能更高效。

🌐 When a piece of information is used for rendering, keep it in state. When a piece of information is only needed by event handlers and changing it doesn’t require a re-render, using a ref may be more efficient.

引用和状态的区别

🌐 Differences between refs and state

也许你会觉得 refs 看起来比 state 不那么“严格”——例如,你可以修改它们,而不必总是使用 state 设置函数。但在大多数情况下,你会希望使用 state。Refs 是一个不常需要使用的“逃生出口”。以下是 state 和 refs 的比较:

🌐 Perhaps you’re thinking refs seem less “strict” than state—you can mutate them instead of always having to use a state setting function, for instance. But in most cases, you’ll want to use state. Refs are an “escape hatch” you won’t need often. Here’s how state and refs compare:

refsstate
useRef(initialValue) returns { current: initialValue }useState(initialValue) returns the current value of a state variable and a state setter function ( [value, setValue])
Doesn’t trigger re-render when you change it.Triggers re-render when you change it.
Mutable—you can modify and update current’s value outside of the rendering process.”Immutable”—you must use the state setting function to modify state variables to queue a re-render.
You shouldn’t read (or write) the current value during rendering.You can read state at any time. However, each render has its own snapshot of state which does not change.

这是一个使用状态实现的计数器按钮:

🌐 Here is a counter button that’s implemented with state:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}

因为显示了 count 的值,因此使用状态值是有道理的。当使用 setCount() 设置计数器的值时,React 会重新渲染组件,屏幕会更新以反映新的计数。

🌐 Because the count value is displayed, it makes sense to use a state value for it. When the counter’s value is set with setCount(), React re-renders the component and the screen updates to reflect the new count.

如果你尝试用 ref 来实现这一点,React 永远不会重新渲染组件,因此你永远看不到计数的变化!看看点击这个按钮是如何不会更新其文本的:

🌐 If you tried to implement this with a ref, React would never re-render the component, so you’d never see the count change! See how clicking this button does not update its text:

import { useRef } from 'react';

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

  function handleClick() {
    // This doesn't re-render the component!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>
  );
}

这就是为什么在渲染期间读取 ref.current 会导致代码不可靠。如果你需要那样做,请改用状态。

🌐 This is why reading ref.current during render leads to unreliable code. If you need that, use state instead.

深入研究

useRef 在内部是如何工作的?

🌐 How does useRef work inside?

尽管 useStateuseRef 都由 React 提供,但原则上 useRef 可以在 useState 之上实现。你可以想象在 React 内部,useRef 是这样实现的:

🌐 Although both useState and useRef are provided by React, in principle useRef could be implemented on top of useState. You can imagine that inside of React, useRef is implemented like this:

// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

在第一次渲染时,useRef 返回 { current: initialValue }。这个对象被 React 存储,所以在下一次渲染时会返回相同的对象。注意在这个例子中状态设置函数没有被使用。这是没有必要的,因为 useRef 总是需要返回相同的对象!

🌐 During the first render, useRef returns { current: initialValue }. This object is stored by React, so during the next render the same object will be returned. Note how the state setter is unused in this example. It is unnecessary because useRef always needs to return the same object!

React 提供了一个内置版本的 useRef,因为在实践中它足够常见。但你可以把它看作一个没有 setter 的普通状态变量。如果你熟悉面向对象编程,refs 可能会让你想起实例字段——但你不是写 this.something,而是写 somethingRef.current

🌐 React provides a built-in version of useRef because it is common enough in practice. But you can think of it as a regular state variable without a setter. If you’re familiar with object-oriented programming, refs might remind you of instance fields—but instead of this.something you write somethingRef.current.

何时使用引用

🌐 When to use refs

通常,当你的组件需要“跳出”React并与外部 API 进行通信时,你会使用 ref——通常是不会影响组件外观的浏览器 API。以下是一些这种罕见情况的例子:

🌐 Typically, you will use a ref when your component needs to “step outside” React and communicate with external APIs—often a browser API that won’t impact the appearance of the component. Here are a few of these rare situations:

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择引用。

🌐 If your component needs to store some value, but it doesn’t impact the rendering logic, choose refs.

引用的最佳实践

🌐 Best practices for refs

遵循这些原则将使你的组件更具可预测性:

🌐 Following these principles will make your components more predictable:

  • 将 refs 视为应急出口。 当你与外部系统或浏览器 API 打交道时,refs 很有用。如果你的大部分应用逻辑和数据流依赖于 refs,你可能需要重新考虑你的方法。
  • 在渲染期间不要读取或写入 ref.current 如果在渲染期间需要一些信息,请改用 state。由于 React 不知道 ref.current 何时变化,即使在渲染期间读取它也会使你的组件行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在首次渲染时设置一次 ref。)

React 状态的限制不适用于 refs。例如,状态表现得像每次渲染的快照并且不会同步更新。但是当你修改 ref 的当前值时,它会立即改变:

🌐 Limitations of React state don’t apply to refs. For example, state acts like a snapshot for every render and doesn’t update synchronously. But when you mutate the current value of a ref, it changes immediately:

ref.current = 5;
console.log(ref.current); // 5

这是因为**ref 本身是一个普通的 JavaScript 对象,**因此它的行为就像普通对象一样。

🌐 This is because the ref itself is a regular JavaScript object, and so it behaves like one.

当你使用 ref 时,你也不需要担心避免修改。只要你正在修改的对象不用于渲染,React 不会在意你对 ref 或其内容做了什么。

🌐 You also don’t need to worry about avoiding mutation when you work with a ref. As long as the object you’re mutating isn’t used for rendering, React doesn’t care what you do with the ref or its contents.

引用和 DOM

🌐 Refs and the DOM

你可以将 ref 指向任何值。然而,ref 最常见的用例是访问 DOM 元素。例如,如果你想以编程方式聚焦一个输入框,这会非常方便。当你在 JSX 中将 ref 传递给 ref 属性时,比如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current。一旦该元素从 DOM 中被移除,React 会将 myRef.current 更新为 null。你可以在 Manipulating the DOM with Refs. 中阅读更多相关内容。

🌐 You can point a ref to any value. However, the most common use case for a ref is to access a DOM element. For example, this is handy if you want to focus an input programmatically. When you pass a ref to a ref attribute in JSX, like <div ref={myRef}>, React will put the corresponding DOM element into myRef.current. Once the element is removed from the DOM, React will update myRef.current to be null. You can read more about this in Manipulating the DOM with Refs.

回顾

  • Refs 是一种逃生出口,用于保存不用于渲染的值。你不会经常需要它们。
  • ref 是一个普通的 JavaScript 对象,只有一个名为 current 的属性,你可以读取或设置它。
  • 你可以通过调用 useRef Hook 来要求 React 给你一个 ref。
  • 与状态一样,引用允许你在组件重新渲染之间保留信息。
  • 与 state 不同,设置 ref 的 current 值不会触发重新渲染。
  • 在渲染期间不要读取或写入 ref.current。这会使你的组件难以预测。

挑战 1 of 4:
修复损坏的聊天输入

🌐 Fix a broken chat input

输入一条消息并点击“发送”。你会注意到,在看到“已发送!”提示之前有三秒的延迟。在这段延迟期间,你可以看到一个“撤销”按钮。点击它。这个“撤销”按钮应该阻止“已发送!”消息的出现。它通过调用clearTimeout来使用在handleSend期间保存的超时ID来实现这一点。然而,即使点击了“撤销”,“已发送!”消息仍然会出现。找出为什么它不起作用,并修复它。

🌐 Type a message and click “Send”. You will notice there is a three second delay before you see the “Sent!” alert. During this delay, you can see an “Undo” button. Click it. This “Undo” button is supposed to stop the “Sent!” message from appearing. It does this by calling clearTimeout for the timeout ID saved during handleSend. However, even after “Undo” is clicked, the “Sent!” message still appears. Find why it doesn’t work, and fix it.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Sent!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Undo
        </button>
      }
    </>
  );
}