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
}
'current' と書かれた矢印が 'ref' と書かれたポケットに詰め込まれている。

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 の比較は以下の通りです。

refstate
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 を使用してください。

さらに深く知る

useRef の内部動作

useStateuseRef は両方とも React によって提供される機能ですが、本質的には useRefuseState をベースに実装されているものです。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 に入れます。その要素が DOM から削除されると、React は myRef.currentnull にセットします。これについては、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>
      }
    </>
  );
}