ref で値を参照する
コンポーネントに情報を「記憶」させたいが、その情報が新しいレンダーをトリガしないようにしたい場合、ref を使うことができます。
このページで学ぶこと
- コンポーネントに ref を追加する方法
- ref の値を更新する方法
- ref と state の違い
- ref を安全に使う方法
コンポーネントに ref を追加する
コンポーネントに ref を追加するには、React から useRef
フックをインポートします。
import { useRef } from 'react';
コンポーネント内で、useRef
フックを呼び出し、唯一の引数として参照したい初期値を渡します。例えば、値 0
を参照する ref は以下のようになります。
const ref = useRef(0);
useRef
は以下のようなオブジェクトを返します。
{
current: 0 // The value you passed to useRef
}
Illustrated by Rachel Lee Nabors
ref の現在の値には、ref.current
プロパティを通じてアクセスできます。この値は意図的にミュータブル、つまり読み書きが可能となっています。これは、React が管理しない、コンポーネントの秘密のポケットのようなものです。(そしてこれが、ref が React の一方向データフローからの「避難ハッチ (escape hatch)」である理由です。詳細は以下で説明します!)
この例では、ボタンがクリックされるたびに 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 は current
プロパティを読み書きできるだけのプレーンな JavaScript オブジェクトです。
インクリメントごとにコンポーネントが再レンダーされないことに注意してください。state と同様に、ref は React によって再レンダー間で保持されます。ただし、state はセットするとコンポーネントが再レンダーされます。ref を変更しても再レンダーは起きません!
例:ストップウォッチの作成
ref と state を 1 つのコンポーネントで組み合わせることができます。例えば、ユーザがボタンを押すことで開始または停止できるストップウォッチを作成しましょう。ユーザが “Start” を押してからどれだけの時間が経過したかを表示するためには、“Start” ボタンが押された時刻と現在時刻を管理する必要があります。これらの情報はレンダーに使用されるものなので、state に保持します。
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
ユーザが “Start” を押すと、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> </> ); }
“Stop” ボタンが押されると、既存のインターバルをキャンセルして now
という state 変数の更新を停止する必要があります。これは 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> </> ); }
情報がレンダー時に使用される場合は、state に保持します。情報がイベントハンドラ内でのみ必要で、変更しても再レンダーが必要ない場合は、ref を使用する方が効率的です。
ref と state の違い
ref の方が state よりも「制限が緩い」と感じるかもしれません。例えば、state セッタ関数を使わずに変更できるわけですから。しかし、ほとんどの場合、state を使用することになります。ref は頻繁には必要としない「避難ハッチ」です。state と ref の比較は以下の通りです。
ref | 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
を組み込み機能として提供しているのは、これが現実的によくある使用法だからです。しかし、ref をセッタのない通常の state 変数と考えることができます。オブジェクト指向プログラミングに慣れている場合、ref はインスタンスフィールドに似ていると感じるかもしれませんが、this.something
の代わりに somethingRef.current
と書きます。
ref を使うタイミング
通常、ref を使用するのは、コンポーネントが React の外に「踏み出して」、外部 API(多くの場合はコンポーネントの外観に影響を与えないブラウザ API)と通信する必要がある場合です。以下は、そのような稀な状況の例です。
コンポーネントが値を保存する必要があるがそれがレンダーロジックに影響しないという場合は、ref を選択してください。
ref のベストプラクティス
以下の原則に従うことで、コンポーネントがより予測可能になります。
- ref を避難ハッチ (escape hatch) として扱う。ref が有用なのは、外部システムやブラウザ API と連携する場合です。アプリケーションのロジックやデータフローの多くが ref に依存しているような場合は、アプローチを見直すことを検討してください。
- レンダー中に
ref.current
を読み書きしない。レンダー中に情報が必要な場合は、代わりに state を使用してください。React はref.current
が書き換わったタイミングを把握しないため、レンダー中にただそれを読みこむだけでも、コンポーネントの挙動が予測しづらくなってしまいます。(唯一の例外はif (!ref.current) ref.current = new Thing()
のような、最初のレンダー中に一度だけ ref をセットするコードです。)
React の state の制約は ref には適用されません。例えば、state は各レンダーのスナップショットのように振る舞い、同期的に更新されません。しかし、ref の現在値を書き換えると、すぐに変更されます。
ref.current = 5;
console.log(ref.current); // 5
これは、ref 自体は通常の JavaScript オブジェクトに過ぎず、現にそのように振る舞うからです。
また、ref を使っている場合は、ミューテーションを避けることを考慮する必要もありません。書き換えようとしているオブジェクトがレンダーに使われない限り、React は ref やその内容に対してあなたが何を行っても気にしません。
ref と DOM
ref には任意の値を指すことができます。ただし、ref の最も一般的な使用例は、DOM 要素にアクセスすることです。例えば、プログラムで入力にフォーカスを当てたい場合に便利です。<div ref={myRef}>
のようにして JSX の ref
属性に ref を渡すと、React は対応する DOM 要素を myRef.current
に入れます。これについては、ref で DOM を操作するで詳しく説明しています。
まとめ
- ref は、レンダーに使用されない値を保持するための避難ハッチである。これは頻繁には必要ない。
- ref は、
current
という単一のプロパティを持つプレーンな JavaScript オブジェクトであり、読み取りや書き込みができる。 useRef
フックを呼び出すことで、React に ref を渡してもらう。- state と同様に、ref はコンポーネントの再レンダー間で情報を保持することができる。
- state とは異なり、ref の
current
値をセットしても再レンダーはトリガされない。 - レンダー中に
ref.current
を読み書きしてはならない。それをするとコンポーネントが予測困難になる。
チャレンジ 1/4: 壊れたチャット入力欄を修正
メッセージを入力して “Send” をクリックしてください。“Sent!” アラートが表示されるまでに 3 秒の遅延があることに気付くでしょう。この遅延中に “Undo” ボタンが表示されます。それをクリックしてください。この “Undo” ボタンは、handleSend
中で保存されたタイムアウト ID に対して clearTimeout
を呼び出すことで、“Sent!” メッセージが表示されないようにするはずのものです。しかし、“Undo” をクリックしても “Sent!” メッセージが表示されてしまいます。動作しない理由を探し、修正してください。
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> } </> ); }