React + TypeScript: refで値を参照する

React公式サイトのドキュメントが2023年3月16日に改訂されました(「[Introducing react.dev](https://react.dev/blog/2023/03/16/introducing-react-dev)」参照)。本稿は、応用解説の「[Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs)」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。

なお、本シリーズ解説の他の記事については「[React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ](https://qiita.com/FumioNonaka/items/d70585bd3c0a6d4edc24)」をご参照ください。

コンポーネントに何か情報を「記憶」させつつ、その情報による[新たなレンダー](https://qiita.com/FumioNonaka/items/e7e285c1bae049706a58)は引き起こしたくないとき使えるのが`ref`です。


# コンポーネントに`ref`を加える
コンポーネントに`ref`を加えるには、`useRef`フックが`import`されていなければなりません。フックの呼び出しに与える引数はひとつで、`ref`が参照する初期値です。たとえば、つぎのコードでは数値`0`を初期値としました。

```react
import { useRef } from 'react';

export default function App() {
	const ref = useRef(0);

}
```

このとき、`useRef`が返すのは、つぎのようなオブジェクトです。

```react
{ 
	current: 0 // useRefに渡した初期値
}
```

現在の値は、`ref.current`プロパティから参照してください。このプロパティ値はあえてミュータブルです。つまり、値は読み書きともにできます。Reactが追跡しない、コンポーネントの秘密のポケットのようなものでしょう(Reactの一方向データフローの「非常口」とされる理由です)。

つぎのコードは、ボタンクリックのたびに`ref.current`の値を加算します(サンプル001)。

```react:src/App.tsx
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>
	);
}
```

#### サンプル001■React + TypeScript: Referencing Values with Refs 01

https://codesandbox.io/embed/react-typescript-referencing-values-with-refs-01-my585g?fontsize=14&hidenavigation=1&theme=dark

このコードで、`ref`が参照しているのは数値です。けれど、[状態](https://qiita.com/FumioNonaka/items/0961f93d9dcefca7f4dc)と同じく値とするデータは問いません(文字列やオブジェクト、さらには関数でも参照できます)。状態と異なるのは、`ref`が単純なJavaScriptオブジェクトで、`current`プロパティの値を取得・変更できることです。

なお、この例では**値を増やしても、そのたびにコンポーネントが再レンダリングされない**ことにご注意ください。状態と同じように、`ref`は各再レンダリングの間Reactが保持します。ただし、状態の設定がコンポーネントを再レンダリングするのに対し、`ref`の変更にレンダーはともないません。


# 例: ストップウォッチをつくる
`ref`と状態は、ひとつのコンポーネントの中で組み合わせて使えます。たとえば、ストップウォッチをつくってみましょう。ボタンを押して開始と停止ができます。そして、開始ボタンが押されてからの経過時間を示さなければなりません。そのためには、いつ開始ボタンが押されたかと、現在の時刻を把握すべきです。**この情報はレンダリングに用いるので、状態にもたせます**。

開始ボタンが押されたら、[`setInterval`](https://developer.mozilla.org/docs/Web/API/setInterval)で10ミリ秒ごとに時間を更新しましょう(サンプル002)。

```react:src/App.tsx
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>
		</>
	);
}
```

#### サンプル002■React + TypeScript: Referencing Values with Refs 02

https://codesandbox.io/embed/react-typescript-referencing-values-with-refs-02-lg8m7x?fontsize=14&hidenavigation=1&theme=dark

さらに加えるのは停止ボタンです。押されたときに、今のインターバルは解除して、状態変数`now`の更新を止めます。解除するために呼び出すのは、[`clearInterval`](https://developer.mozilla.org/docs/Web/API/clearInterval)です。ただし、引数には解除するインターバルのIDを渡す必要があります。IDは、開始ボタンを押して呼び出された`setInterval`の戻り値です。そのため、インターバルIDも保持しておきましょう。**IDはレンダリングには用いられません**。**こういうときに使うのが`ref`です**(サンプル003)。

```react:src/App.tsx
// 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: Referencing Values with Refs 03

https://codesandbox.io/embed/react-typescript-referencing-values-with-refs-03-vn56wc?fontsize=14&hidenavigation=1&theme=dark

なお、TypeScriptを使うときは、`setInterval`の呼び出しに`window`の参照が必要になります。NodeJSの型`@types/node`に依存している場合、`window`を省くと戻り値が(`number`でなく)`NodeJS.Timer`と推論されてしまからです(「[TypeScriptでsetInterval()の型が合わない理由と解決方法](https://blog.kubosho.com/entries/setinterval-trap-on-typescript)」参照)。

情報がレンダリングに用いられるときは、状態に保持してください。イベントハンドラが必要とするだけで、変更しても再レンダーの要らない情報には、`ref`を使うと効率的なこともあるでしょう。


# `ref`と状態の違い
`ref`は状態と比べて「厳格性」に欠けると感じたかもしれません。`ref`を変更するには、状態のような設定関数などまったく使わずに済みます。もっとも、ほとんどの場合には、状態が用いられるでしょう。`ref`はあくまで「非常口」で、頻繁に必要とはされません。`ref`と状態を比べたのがつぎの表です。


| `ref` | 状態 |
|:--|:--|
| `useRef(initialValue)`はオブジェクト`{ current: initialValue }`を返します。 | `useState(initialValue)`の戻り値は、状態変数の現在値と状態設定関数の配列です(`[value, setValue]`)。 |
| `ref`の値を変えても、レンダーは起こりません。 | 状態の変更は、つねに再レンダーされます。 |
| ミュータブル: `current`プロパティ値の変更と更新は、レンダリングプロセスの外です。 | イミュータブル: 状態変数は設定関数で変更して、再レンダリングのキューに入れなければなりません。 |
| レンダリングの間は`current`の値を読み書きしないでください。 | 状態の値はいつでも読み込めます。ただし、状態はレンダリングごとの[スナップショット](https://qiita.com/FumioNonaka/items/de813331817014bac2e6)なので、個々のレンダリングの値は変わりません。 |

`ref`と状態の違いをコードで比べてみましょう。つぎのカウンターは状態を用いた場合です(サンプル004)。`count`の値は表示されます。したがって、状態変数を使うのが適しているでしょう。カウンターの値が`setCount()`関数で設定されれば、Reactはコンポーネントを再レンダーし、画面は更新されてカウンターの新たな値に反映されるのです。

```react:src/App.tsx
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: Referencing Values with Refs 04

https://codesandbox.io/embed/react-typescript-referencing-values-with-refs-04-hvgt9g?fontsize=14&hidenavigation=1&theme=dark

このカウンターを`ref`で実装しようとしたのがつぎのコードです(サンプル005)。Reactは`ref`の値が変わってもコンポーネントを再レンダリングしません。つまり、画面でボタンをクリックしても、**カウンターの数値が更新されない**のです。けれど、コンソールを見れば`console.log()`で**出力される`countRef.current`の値は変わっている**ことが確かめられるでしょう。

```react:src/App.tsx
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>
	);
}
```

#### サンプル005■React + TypeScript: Referencing Values with Refs 05

https://codesandbox.io/embed/react-typescript-referencing-values-with-refs-05-yj3fz7?fontsize=14&hidenavigation=1&theme=dark

なお、レンダリング中に`ref.current`を読み込むと、コードに問題が生じるかもしれません。そのようなときは、状態をお使いください。

## `useRef`の内部的な動き
`useState`と`useRef`は、Reactの内部的に備わるフックです。けれど、原理としては`useRef`は`useState`で実装することもできます。`useRef`フックをコードで示すなら、つぎのとおりです。

```react
const useRef = (initialValue: any) => {
	const [ref] = useState({ current: initialValue });
	return ref;
};
```

最初のレンダリングで、`useRef`は`{ current: initialValue }`を返します。参照はReactに保持されるので、つぎのレンダリング時に返されるオブジェクトもそのまま変わりません。コード例に状態の設定関数がないことにご注目ください。これは、`useRef`がつねに同じオブジェクトを返すからです。


# `ref`はいつ使うのか
通常、`ref`を用いるのは、コンポーネントがReactのデータフローから「外に出て」外部APIと通信しなければならないときでしょう。大抵はブラウザAPIで、コンポーネントの外観には影響しない場合です。そのような、数少ない状況をご紹介します。

* [タイムアウトID](https://developer.mozilla.org/docs/Web/API/setTimeout)を保持するときです。
* [DOM要素](https://developer.mozilla.org/docs/Web/API/Element)を保持して操作したい場合が挙げられます。
* JSXの算出が要らないその他のオブジェクトの保持です。

コンポーネントに保持したい値があって、レンダリングロジックには影響しない場合に`ref`を選択しましょう。


# `ref`の適切な使い方
つぎのふたつの原則にしたがうと、コンポーネントは適切に扱えます。

* **`ref`は「非常口」です**。外部システムやブラウザAPIを扱うのに役立つでしょう。けれど、アプリケーションロジックやデータフローの多くが`ref`に依存しているなら、設計を考え直すべきかもしれません。
* **レンダリング中に`ref.current`を読み書きしないでください**。レンダリングしている間に必要な情報には、代わりに状態を使いましょう。Reactは`ref.current`がいつ変更されるか知りません。その情報をレンダリングのときに読み取るだけでも、コンポーネントの動きは予測しにくくなります。
    - ひとつの例外は、はじめのレンダリング時につぎのように一度だけ初期値を与える場合です。
    - `if (!ref.current) ref.current = new Thing()`

Reactの状態の制限は`ref`には適用されません。状態の動きは[スナップショット](https://qiita.com/FumioNonaka/items/de813331817014bac2e6)です。[同期的には更新されません](https://qiita.com/FumioNonaka/items/73cb59fb3cabce4106da)。それに対して、`ref`の`current`の値は、変更が直ちに反映されます。

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

これは、**`ref`そのものが標準のJavaScriptオブジェクトだからです**。したがって、オブジェクトとして動作します。

また、ミューテーションを避けることは、`ref`の扱いでは気にしなくて構いません。変更するオブジェクトがレンダリングに使われないかぎり、Reactは`ref`やその内容の扱いに関知しないからです。


# `ref`とDOM
`ref`はどのような値でも参照できます。けれど、もっともよく用いられるのは、DOM要素の取得でしょう。たとえば、`<input>`要素にプログラムでフォーカスを与えたいときなどです。`ref`はJSXで要素の`ref`属性に`<div ref={myRef}>`のように渡します。すると、ReactはそのDOM要素を`myRef.current`に収めるのです。`ref`を用いたDOMの操作は記事を改めて解説します。


# まとめ
この記事では、つぎのような項目についてご説明しました。

* `ref`はレンダリングには使われない値を保持する「非常口」です。たびたび用いるものではありません。
* `ref`は`current`というプロパティをひとつだけもつ単純な標準JavaScriptオブジェクトです。プロパティ値は読み書きできます。
* Reactに対しては、`useRef`フックの呼び出しで`ref`を取得してください。
* 状態と同じように、`ref`は情報をコンポーネントの再レンダリング間で保持できます。
* 状態と異なり、`ref`の`current`値を設定しても、再レンダーされません。
* レンダリング中に`ref.current`を読み書きしないでください。コンポーネントの動きを予測するのが難しくなります。