使用引用引用值

当你想要一个组件 “记住” 一些信息,但你不希望信息 触发新渲染 时,你可以使用引用。

¥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.

你将学习到

  • 如何向组件添加引用

    ¥How to add a ref to your component

  • 如何更新引用的值

    ¥How to update a ref’s value

  • 引用与状态有何不同

    ¥How refs are different from state

  • 如何安全地使用引用

    ¥How to use refs safely

向你的组件添加引用

¥Adding a ref to your component

你可以通过从 React 导入 useRef 钩子来为你的组件添加一个引用:

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

import { useRef } from 'react';

在你的组件内,调用 useRef 钩子并将你要引用的初始值作为唯一参数传递。例如,这是对值 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 返回一个这样的对象:

¥useRef returns an object like this:

{
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 属性访问该引用的当前值。这个值是有意可变的,这意味着你可以读取和写入它。它就像是 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>
  );
}

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

¥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.

请注意,组件不会随着每次增量而重新渲染。与状态一样,引用在重新渲染之间由 React 保留。但是,设置状态会重新渲染组件。更改引用不会!

¥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

你可以将引用和状态组合在一个组件中。例如,让我们制作一个秒表,用户可以通过按下按钮来启动或停止。为了显示自用户按下 “开始” 以来经过了多长时间,你需要跟踪按下“开始”按钮的时间以及当前时间。此信息用于渲染,因此你将保持它的状态:

¥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 不用于渲染,你可以将其保存在引用中:

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

当一条信息用于渲染时,保留它的状态。当一条信息仅由事件处理程序需要并且更改它不需要重新渲染时,使用引用可能更有效。

¥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

也许你认为引用看起来比状态少 “严格” — 例如,你可以改变它们,而不必总是使用状态设置函数。但在大多数情况下,你会希望使用状态。引用是你不会经常需要的 “应急方案”。以下是状态和引用的比较:

¥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:

引用状态
useRef(initialValue) 返回 { current: initialValue }useState(initialValue) 返回状态变量的当前值和状态设置函数 ([value, setValue])
更改时不会触发重新渲染。当你更改它时,触发器会重新渲染。
可变 - 你可以在渲染过程之外修改和更新 current 的值。“不变的”—你必须使用状态设置功能来修改状态变量以对重新渲染进行排队。
你不应在渲染期间读取(或写入)current 值。你可以随时读取状态。但是,每个渲染器都有自己的 快照 状态,不会改变。

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

¥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.

如果你试图用一个引用来实现它,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 的内置版本,因为它在实践中很常见。但是你可以将其视为没有设置器的常规状态变量。如果你熟悉面向对象的编程,引用可能会让你想起实例字段 - 但是你写的不是 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(通常是不会影响组件外观的浏览器 API)通信时,你将使用 ref。以下是其中一些罕见的情况:

¥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:

  • 将引用视为应急方案。当你使用外部系统或浏览器 API 时,引用很有用。如果你的大部分应用逻辑和数据流都依赖于引用,你可能需要重新考虑你的方法。

    ¥Treat refs as an escape hatch. Refs are useful when you work with external systems or browser APIs. If much of your application logic and data flow relies on refs, you might want to rethink your approach.

  • 不要在渲染过程中读取或写入 ref.current。如果在渲染过程中需要一些信息,请改用 状态。由于 React 不知道 ref.current 何时更改,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它在第一次渲染期间只设置一次引用。)

    ¥Don’t read or write ref.current during rendering. If some information is needed during rendering, use state instead. Since React doesn’t know when ref.current changes, even reading it while rendering makes your component’s behavior difficult to predict. (The only exception to this is code like if (!ref.current) ref.current = new Thing() which only sets the ref once during the first render.)

React 状态的限制不适用于引用。例如,状态就像 每个渲染的快照不同步更新。 但是当你改变引用的当前值时,它会立即改变:

¥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

这是因为引用本身是一个常规的 JavaScript 对象,因此它的行为就像一个。

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

当你使用引用时,你也不必担心 避免突变。只要你正在改变的对象不用于渲染,React 就不会关心你对引用或其内容做了什么。

¥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

你可以将引用指向任何值。但是,引用最常见的用例是访问 DOM 元素。例如,如果你想以编程方式聚焦输入,这会很方便。当你将引用传递给 JSX 中的 ref 属性时,如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current。一旦元素从 DOM 中删除,React 就会将 myRef.current 更新为 null。你可以在 使用引用操作 DOM 中阅读更多相关信息。

¥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 are an escape hatch to hold onto values that aren’t used for rendering. You won’t need them often.

  • 引用是一个纯 JavaScript 对象,具有一个名为 current 的属性,你可以读取或设置它。

    ¥A ref is a plain JavaScript object with a single property called current, which you can read or set.

  • 你可以通过调用 useRef 钩子请求 React 给你一个引用。

    ¥You can ask React to give you a ref by calling the useRef Hook.

  • 与状态一样,引用允许你在组件重新渲染之间保留信息。

    ¥Like state, refs let you retain information between re-renders of a component.

  • 与状态不同,设置引用的 current 值不会触发重新渲染。

    ¥Unlike state, setting the ref’s current value does not trigger a re-render.

  • 不要在渲染过程中读取或写入 ref.current。这使你的组件难以预测。

    ¥Don’t read or write ref.current during rendering. This makes your component hard to predict.

挑战 1 / 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>
      }
    </>
  );
}


React 中文网 - 粤ICP备13048890号