LoginSignup
2
1

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

Last updated at Posted at 2023-06-12

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

なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。

コンポーネントに何か情報を「記憶」させつつ、その情報による新たなレンダーは引き起こしたくないとき使えるのがrefです。

コンポーネントにrefを加える

コンポーネントにrefを加えるには、useRefフックがimportされていなければなりません。フックの呼び出しに与える引数はひとつで、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)。

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

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

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

例: ストップウォッチをつくる

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

開始ボタンが押されたら、setIntervalで10ミリ秒ごとに時間を更新しましょう(サンプル002)。

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

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

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

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

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

refと状態の違い

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

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

refと状態の違いをコードで比べてみましょう。つぎのカウンターは状態を用いた場合です(サンプル004)。countの値は表示されます。したがって、状態変数を使うのが適しているでしょう。カウンターの値がsetCount()関数で設定されれば、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

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

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

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

useRefの内部的な動き

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

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

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

refはいつ使うのか

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

  • タイムアウトIDを保持するときです。
  • DOM要素を保持して操作したい場合が挙げられます。
  • JSXの算出が要らないその他のオブジェクトの保持です。

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

refの適切な使い方

つぎのふたつの原則にしたがうと、コンポーネントは適切に扱えます。

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

Reactの状態の制限はrefには適用されません。状態の動きはスナップショットです。同期的には更新されません。それに対して、refcurrentの値は、変更が直ちに反映されます。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1