当你想让组件“记住”一些信息,但又不想让这些信息触发重新渲染时,可以使用ref。
你将学习
- 如何向你的组件添加 ref
- 如何更新 ref 的值
- ref 与状态有何不同
- 如何安全地使用 ref
向你的组件添加 ref
你可以通过从 React 中导入 useRef
Hook 来向你的组件添加 ref。
import { useRef } from 'react';
在你的组件内部,调用 useRef
Hook 并将你想要引用的初始值作为唯一参数传递。例如,这是一个对值 0
的 ref。
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 指向一个数字,但是,就像状态一样,你可以指向任何内容:字符串、对象,甚至是函数。与状态不同,ref 是一个带有 current
属性的普通 JavaScript 对象,你可以读取和修改它。
请注意,组件不会在每次递增时都重新渲染。与状态一样,ref 在重新渲染之间由 React 保留。但是,设置状态会重新渲染组件。更改 ref 不会!
示例:构建秒表
你可以在单个组件中组合 ref 和状态。例如,让我们制作一个用户可以通过按下按钮来启动或停止的秒表。为了显示自用户按下“启动”以来经过了多少时间,你需要跟踪按下“启动”按钮的时间以及当前时间。此信息用于渲染,因此你将将其保存在状态中:
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> </> ); }
当按下“停止”按钮时,你需要取消现有间隔,以便它停止更新 now
状态变量。你可以通过调用clearInterval
来执行此操作,但是你需要向其提供之前由 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 可能更有效。
ref 和状态之间的区别
也许您觉得 refs 比 state 看起来不那么“严格”——例如,您可以修改它们,而不必总是使用 state 设置函数。但在大多数情况下,您应该使用 state。refs 是您通常不需要的“应急出口”。以下是 state 和 refs 的比较:
refs | state |
---|---|
useRef(initialValue) 返回 { current: initialValue } | useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数([value, setValue] ) |
更改时不会触发重新渲染。 | 更改时会触发重新渲染。 |
可变的——您可以在渲染过程之外修改和更新 current 的值。 | “不可变的”——您必须使用 state 设置函数来修改 state 变量以排队重新渲染。 |
您不应该在渲染过程中读取(或写入)current 值。 | 您可以随时读取 state。但是,每次渲染都有其自己的 快照 state,它不会改变。 |
这是一个使用 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
值会显示,所以使用 state 值是有意义的。当使用 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
会导致代码不可靠。如果您需要这样做,请改用 state。
深入探讨
尽管 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 存储,因此在下一次渲染期间将返回相同的对象。请注意,在此示例中 state 设置器未使用。这是不必要的,因为 useRef
总是需要返回相同的对象!
React 提供了 useRef
的内置版本,因为它在实践中足够常见。但您可以将其视为没有设置器的普通 state 变量。如果您熟悉面向对象编程,refs 可能会让您想起实例字段——但是您编写的是 somethingRef.current
,而不是 this.something
。
何时使用 refs
通常,当您的组件需要“跳出”React 并与外部 API 通信时,您会使用 ref——通常是不会影响组件外观的浏览器 API。以下是一些罕见的情况:
如果您的组件需要存储一些值,但它不会影响渲染逻辑,请选择 refs。
refs 的最佳实践
遵循这些原则将使您的组件更可预测。
- 将 refs 视为应急出口。 当您使用外部系统或浏览器 API 时,refs 很有用。如果您的应用程序逻辑和数据流很大程度上依赖于 refs,您可能需要重新考虑您的方法。
- 不要在渲染期间读取或写入
ref.current
。 如果在渲染期间需要某些信息,请改用 state。由于 React 不知道ref.current
何时更改,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是类似if (!ref.current) ref.current = new Thing()
的代码,它只在第一次渲染期间设置一次 ref。)
React state 的限制不适用于 refs。例如,state 充当 每次渲染的快照 并且 不会同步更新。 但是当您修改 ref 的当前值时,它会立即更改。
ref.current = 5;
console.log(ref.current); // 5
这是因为 ref 本身就是一个普通的 JavaScript 对象,因此它的行为就像一个普通的 JavaScript 对象。
当您使用 ref 时,您也不需要担心 避免变异。 只要您正在变异的对象不用于渲染,React 就不会关心您对 ref 或其内容做了什么。
Refs 和 DOM
你可以将 ref 指向任何值。但是,ref 最常见的用例是访问 DOM 元素。例如,如果你想以编程方式聚焦一个输入框,这将非常方便。当你将 ref 传递给 JSX 中的 ref
属性,例如 <div ref={myRef}>
时,React 将把相应的 DOM 元素放入 myRef.current
中。一旦元素从 DOM 中移除,React 将更新 myRef.current
为 null
。你可以在 使用 Refs 操作 DOM 中了解更多信息。
回顾
- Refs 是一种用来保存不参与渲染的值的应急手段。你通常不需要经常用到它们。
- Ref 是一个普通的 JavaScript 对象,它只有一个名为
current
的属性,你可以读取或设置它的值。 - 你可以通过调用
useRef
Hook 来让 React 提供一个 ref 给你。 - 与状态一样,refs 允许你在组件重新渲染之间保留信息。
- 与状态不同的是,设置 ref 的
current
值不会触发重新渲染。 - 不要在渲染过程中读取或写入
ref.current
。这会使你的组件难以预测。
挑战 1的 4: 修复损坏的聊天输入框
输入一条消息并点击“发送”。你会注意到在看到“已发送!”警报之前有 3 秒的延迟。在此延迟期间,你可以看到一个“撤回”按钮。点击它。“撤回”按钮应该阻止“已发送!”消息出现。它通过在 handleSend
期间保存的超时 ID 上调用 clearTimeout
来做到这一点。但是,即使点击了“撤回”,“已发送!”消息仍然会出现。找出它为什么不起作用,并修复它。
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> } </> ); }