使用React + TypeScript时,通过ref引用值
React官方网站的文档已于2023年3月16日进行了修订(请参阅”Introducing react.dev”)。本文简要概括了应用解析中”Referencing Values with Refs”的文章。但是,在代码中加入了TypeScript。另外,省略了针对初学者的JavaScript基础解释。
此外,请参阅其他本系列解说文章,标题为《React+TypeScript:学习React官方文档基本解说Learn React》。
当我们需要在组件中存储某些信息,但又不想借此信息触发新的渲染时,就可以使用ref。
向组件添加ref
要给组件添加ref,需要import useRef hook。传递给hook调用的参数只有一个,它是ref引用的初始值。例如,在下面的代码中,我们将其设置为0。
import { useRef } from 'react';
export default function App() {
const ref = useRef(0);
}
在这种情况下,useRef返回的是以下类似的对象。
{
current: 0 // useRefに渡した初期値
}
请从ref.current属性中引用当前值。这个属性的值是故意可变的。换句话说,该值可以读取和写入。这就像是React不跟踪的组件的秘密口袋(这就是React的单向数据流的“紧急出口”)。
下面的代码是每次点击按钮时对ref.current的值进行累加(示例001)。
import { useRef } from 'react';
export default function Counter() {
const ref = useRef(0);
const handleClick = () => {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
};
return (
<button onClick={handleClick}>Click me!</button>
);
}
React + TypeScript: 通过引用引用值
在这段代码中,ref引用的是一个数字。但是,它可以引用任何值类型的数据(如字符串、对象甚至函数)。与状态不同的是,ref是一个简单的JavaScript对象,可以获取和修改current属性的值。
在这个例子中,请注意每次增加值时,组件不会重绘。与状态一样,ref 在 React 在每次重绘之间保持不变。但是,与设置状态会重新渲染组件不同,ref 的更改不会引起渲染。
制作一个秒表
ref和状态可以在一个组件中组合使用。例如,让我们尝试制作一个秒表。我们可以按下按钮来开始和停止计时。而且,我们需要显示从按下开始按钮开始经过的时间。为了做到这一点,我们需要知道开始按钮何时被按下以及当前的时间。这些信息将用于渲染,因此它们应该存储在状态中。
一旦点击了开始按钮,让我们使用setInterval每10毫秒更新一次时间(样例002)。
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState<number | null>(null);
const [now, setNow] = useState<number | null>(null);
const handleStart = () => {
// カウント開始
setStartTime(Date.now());
setNow(Date.now());
setInterval(() => {
// 10ミリ秒ごとに更新
setNow(Date.now());
}, 10);
};
const secondsPassed =
startTime !== null && now !== null ? (now - startTime) / 1000 : 0;
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>Start</button>
</>
);
}
React + TypeScript示例002:使用Refs引用值
另外加入的是停止按钮。当按钮被按下时,当前的间隔将被解除,更新状态变量now的更新将停止。要解除,请调用clearInterval。但是,需要传递解除间隔的ID作为参数。该ID是通过按下启动按钮并调用setInterval的返回值获得的。因此,也要保存间隔ID。ID不会在渲染中使用。在这种情况下,可以使用ref (示例003)。
// import { useState } from 'react';
import { useRef, useState } from 'react';
export default function Stopwatch() {
const intervalRef = useRef<number | null>(null);
const handleStart = () => {
// setInterval(() => {
intervalRef.current = window.setInterval(() => {
setNow(Date.now());
}, 10);
};
const handleStop = () => {
if (!intervalRef.current) return;
clearInterval(intervalRef.current);
};
}
示例003■React + TypeScript:使用引用引用值03
请注意,当使用TypeScript时,调用setInterval方法需要使用window的引用。如果依赖于NodeJS类型@types/node,如果省略window,则返回值将被推断为NodeJS.Timer而不是number(请参考「TypeScriptでsetInterval()の型が合わない理由と解決方法」)。
在使用信息进行渲染时,请将其保存在状态中。对于只需要事件处理程序所需的信息,可能使用ref是更有效的选择,因为它们不需要重新渲染。
ref和状态的差异
也许您觉得与状态相比,引用(ref)缺乏严谨性。您可以不使用像状态设置功能这样的方式来更改引用。当然,在大多数情况下,还是会使用状态。引用只是一个”应急出口”,并不经常需要。下表是引用和状态的比较。
ref
状態useRef(initialValue)
はオブジェクト{ current: initialValue }
を返します。useState(initialValue)
の戻り値は、状態変数の現在値と状態設定関数の配列です([value, setValue]
)。ref
の値を変えても、レンダーは起こりません。状態の変更は、つねに再レンダーされます。ミュータブル: current
プロパティ値の変更と更新は、レンダリングプロセスの外です。イミュータブル: 状態変数は設定関数で変更して、再レンダリングのキューに入れなければなりません。レンダリングの間はcurrent
の値を読み書きしないでください。状態の値はいつでも読み込めます。ただし、状態はレンダリングごとのスナップショットなので、個々のレンダリングの値は変わりません。让我们用代码来比较ref和状态的不同。下面的示例(Sample004)展示了使用状态的计数器。count的值将被显示出来。因此,使用状态变量是合适的。如果计数器的值在setCount()函数中被设置,React会重新渲染组件,屏幕会更新以反映计数器的新值。
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>You clicked {count} times</button>;
}
样本004■React + TypeScript:通过引用引用值04
尝试用ref来实现这个计数器的代码是以下的样例005。React在ref的值发生变化时不会重新渲染组件。换句话说,即使在屏幕上点击按钮,计数器的数值也不会更新。然而,通过查看控制台,可以确认console.log()输出的countRef.current的值是在变化的。
export default function Counter() {
// const [count, setCount] = useState(0);
const countRef = useRef(0);
const handleClick = () => {
// setCount(count + 1);
// コンポーネントは再レンダリングされない
countRef.current = countRef.current + 1;
console.log('count':, countRef.current);
};
return (
<button onClick={handleClick}>
{/* You clicked {count} times */}
You clicked {countRef.current} times
</button>
);
}
React + TypeScript:使用 Refs 引用值
另外,在渲染过程中读取ref.current可能会导致代码问题。若遇到这种情况,请使用状态来解决。
useRef的内部运作方式
useState和useRef是React内置的钩子。然而,原理上,也可以使用useState来实现useRef。如果要用代码示例来展示useRef钩子,则如下所示。
const useRef = (initialValue: any) => {
const [ref] = useState({ current: initialValue });
return ref;
};
在初始渲染时,useRef将返回{ current: initialValue }。引用被React保存,因此在下一个渲染时返回的对象不会改变。请注意代码示例中没有状态设置功能函数。这是因为useRef始终返回相同的对象。
什么时候要使用ref?
通常情况下,当组件需要从React的数据流中“走出去”与外部API通信时,我们会使用ref。这通常是在使用浏览器API,并且不影响组件外观的少数情况之一。下面将介绍一些这样的情况。
タイムアウトIDを保持するときです。
DOM要素を保持して操作したい場合が挙げられます。
JSXの算出が要らないその他のオブジェクトの保持です。
如果有一个值想要在组件中保持,并且不影响渲染逻辑,那么选择使用 ref。
ref的正确使用方式
遵循以下两个原则,可以适当处理组件。
refは「非常口」です。外部システムやブラウザAPIを扱うのに役立つでしょう。けれど、アプリケーションロジックやデータフローの多くがrefに依存しているなら、設計を考え直すべきかもしれません。
レンダリング中にref.currentを読み書きしないでください。レンダリングしている間に必要な情報には、代わりに状態を使いましょう。Reactはref.currentがいつ変更されるか知りません。その情報をレンダリングのときに読み取るだけでも、コンポーネントの動きは予測しにくくなります。
ひとつの例外は、はじめのレンダリング時につぎのように一度だけ初期値を与える場合です。
if (!ref.current) ref.current = new Thing()
限制React状态的是不适用于ref。状态的变化是快照形式。它不会被同步更新。相反,ref的current值会立即反映任何变化。
ref.current = 5;
console.log(ref.current); // 5
這是因為ref本身是JavaScript標準對象,所以它作為對象運作。
另外,不必担心避免变异对于ref的处理。只要变化的对象不被用于渲染,React就不会关心ref或其内容的处理。
引用和文档对象模型
在中文中有多种方式可以重述上述内容,以下是其中一种选项:
ref可以引用任何值。但最常用的是获取DOM元素。例如,在编程时想要给元素设置焦点时。通过在JSX中传递类似于
的ref属性,将DOM元素存储在myRef.current中。关于如何使用ref操作DOM的内容将在后续的文章中进行解释。
总结
在这篇文章中,我解释了以下内容。
refはレンダリングには使われない値を保持する「非常口」です。たびたび用いるものではありません。
refはcurrentというプロパティをひとつだけもつ単純な標準JavaScriptオブジェクトです。プロパティ値は読み書きできます。
Reactに対しては、useRefフックの呼び出しでrefを取得してください。
状態と同じように、refは情報をコンポーネントの再レンダリング間で保持できます。
状態と異なり、refのcurrent値を設定しても、再レンダーされません。
レンダリング中にref.currentを読み書きしないでください。コンポーネントの動きを予測するのが難しくなります。