第三六章 逃生舱口-使用 ref 引用值
使用 ref 引用值
当你想让一个组件“记住”一些信息,但又不想让这些信息触发新的渲染时,你可以使用 ref。
你将学习
- 如何向组件添加引用
- 如何更新 ref 的值
- refs 与 state 有何不同
- 如何安全地使用 refs
向您的组件添加引用
你可以通过从 React 导入 useRef Hook 来为你的组件添加一个 ref:
import { useRef } from 'react';
在您的组件内,调用 useRef Hook 并将您要引用的初始值作为唯一参数传递。 例如,这是对值 0 的引用:
const ref = useRef(0);
useRef 返回一个这样的对象:
{
current: 0 // The value you passed to useRef
}
您可以通过 ref.current 属性访问该 ref 的当前值。 这个值是有意可变的,这意味着您可以读取和写入它。 它就像 React 不跟踪的组件的秘密口袋。 (这就是使它成为 React 单向数据流的“逃生舱门”的原因——更多内容见下文!)
在这里,一个按钮将在每次点击时增加 ref.current:
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 属性。
请注意,组件不会在每次增量时重新渲染。 与状态一样,refs 在重新渲染之间由 React 保留。 但是,设置状态会重新渲染组件。 更改 ref 不会!
示例:构建秒表
您可以将 refs 和 state 组合在一个组件中。 例如,让我们制作一个秒表,用户可以通过按下按钮来启动或停止。 为了显示自用户按下“开始”以来经过了多长时间,您需要跟踪按下“开始”按钮的时间以及当前时间。 此信息用于渲染,因此您将保持它的状态:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
当用户按下“开始”时,您将使用 setInterval 每 10 毫秒更新一次时间:
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>
</>
);
}
当按下“停止”按钮时,您需要取消现有interval,使其停止更新 now 状态变量。 您可以通过调用 clearInterval 来执行此操作,但您需要为其提供先前在用户按下 Start 时由 setInterval 调用返回的间隔 ID。 您需要将间隔 ID 保存在某处。 由于间隔 ID 不用于渲染,您可以将其保存在 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 可能更有效。
refs 和 state 的区别
也许你在想 refs 似乎没有 state 那么“严格”——你可以改变它们,而不是总是必须使用状态设置函数,例如。 但在大多数情况下,你会想要使用状态。 Refs 是一个你不会经常需要的“逃生口”。 以下是 state 和 refs 的比较:
refs | state |
---|---|
useRef(initialValue) 返回 { current: initialValue }
|
useState(initialValue) 返回状态变量的当前值和状态设置函数 ( [value, setValue] ) |
更改时不会触发重新渲染。 | 当您更改它时,触发器会重新呈现。 |
可变——您可以在渲染过程之外修改和更新 current 的值。 |
“不可变”——你必须使用状态设置函数来修改状态变量来排队重新渲染。 |
您不应该在渲染期间读取(或写入)current 值。 |
您可以随时读取状态。 但是,每个渲染都有自己的状态快照,不会改变。 |
这是一个用状态实现的计数器按钮:
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 重新渲染组件并且屏幕更新以反映新的计数。
如果你试图用 ref 来实现它,React 永远不会重新渲染组件,所以你永远不会看到计数发生变化! 查看单击此按钮如何不更新其文本:
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 会导致代码不可靠。 如果需要,请改用状态。
深度阅读:useRef内部如何工作?
虽然 useState 和 useRef 都是 React 提供的,但原则上 useRef 可以在 useState 之上实现。 你可以想象在 React 内部,useRef 是这样实现的:
// Inside of React function useRef(initialValue) { const [ref, unused] = useState({ current: initialValue }); return ref; }
在第一次渲染期间,useRef 返回 { current: initialValue }。 该对象由 React 存储,因此在下一次渲染期间将返回相同的对象。 请注意此示例中未使用状态设置器的方式。 这是不必要的,因为 useRef 总是需要返回同一个对象!
React 提供了 useRef 的内置版本,因为它在实践中很常见。 但是您可以将其视为没有设置器的常规状态变量。 如果你熟悉面向对象的编程,refs 可能会让你想起实例字段——但你写的不是 this.something 而是somethingRef.current。
何时使用ref
通常,当您的组件需要“跳出”React 并与外部 API 通信时,您将使用 ref——通常是不会影响组件外观的浏览器 API。 以下是其中一些罕见的情况:
- 存储超时 ID
- 存储和操作 DOM 元素,我们将在下一页介绍
- 存储计算 JSX 不需要的其他对象。
如果您的组件需要存储一些值,但不影响渲染逻辑,请选择 refs。
ref的最佳实践
遵循这些原则将使您的组件更具可预测性:
- 将 refs 视为逃生舱口。 当您使用外部系统或浏览器 API 时,引用很有用。 如果您的大部分应用程序逻辑和数据流都依赖于引用,您可能需要重新考虑您的方法。
- 不要在渲染期间读取或写入 ref.current。 如果在渲染过程中需要一些信息,请改用状态。 由于 React 不知道 ref.current 何时更改,即使在渲染时读取它也会使组件的行为难以预测。 (唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)
React 状态的限制不适用于 refs。 例如,状态就像每个渲染的快照,不会同步更新。 但是当你改变 ref 的当前值时,它会立即改变:
ref.current = 5;
console.log(ref.current); // 5
这是因为 ref 本身是一个普通的 JavaScript 对象,所以它的行为就像这样。
当你使用 ref 时,你也不需要担心避免变异。 只要你改变的对象不用于渲染,React 就不会关心你对 ref 或它的内容做了什么。
引用和 DOM
您可以将 ref 指向任何值。 但是,ref 最常见的用例是访问 DOM 元素。 例如,如果您想以编程方式聚焦输入,这会很方便。 当你将 ref 传递给 JSX 中的 ref 属性时,如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current 中。 您可以在使用 Refs 操作 DOM 中阅读更多相关信息。
回顾
- Refs 是一个逃生舱口,用于保存不用于渲染的值。 你不会经常需要它们。
- ref 是一个纯 JavaScript 对象,具有一个名为 current 的属性,您可以读取或设置该属性。
- 你可以通过调用 useRef Hook 让 React 给你一个 ref。
- 与 state 一样,refs 允许您在组件重新渲染之间保留信息。
- 与状态不同,设置 ref 的当前值不会触发重新渲染。
- 不要在渲染期间读取或写入 ref.current。 这使您的组件难以预测。