コンポーネントに何らかの情報を「記憶」させたいものの、その情報によって新しいレンダーをトリガーさせたくない場合、ref を使うことができます。

以下を学びます

  • コンポーネントに ref を追加する方法
  • ref の値を更新する方法
  • refs が state と異なる点
  • refs を安全に使う方法

コンポーネントへのrefの追加

ReactからuseRefフックをインポートすることで、コンポーネントにrefを追加できます

import { useRef } from 'react';

コンポーネント内で、useRefフックを呼び出し、参照したい初期値を唯一の引数として渡します。たとえば、ここに値0へのrefがあります

const ref = useRef(0);

useRefは、次のようなオブジェクトを返します

{
current: 0 // The value you passed to useRef
}
An arrow with 'current' written on it stuffed into a pocket with 'ref' written on it.

イラスト: Rachel Lee Nabors

ref.currentプロパティを介して、そのrefの現在の値にアクセスできます。この値は意図的に可変であり、読み取りも書き込みも可能です。Reactが追跡しないコンポーネントの秘密のポケットのようなものです。(これが、Reactの一方向データフローからの「エスケープハッチ」となる理由です。詳細は後述します!)

ここでは、ボタンをクリックするたびに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と同様に、refsは再レンダー間でReactによって保持されます。ただし、stateを設定するとコンポーネントが再レンダーされます。refを変更しても再レンダーされません!

例:ストップウォッチの構築

1つのコンポーネントでrefsとstateを組み合わせることができます。たとえば、ボタンを押すことで開始または停止できるストップウォッチを作成してみましょう。「開始」ボタンが押されてからの経過時間を表示するには、「開始」ボタンが押された時刻と現在の時刻を追跡する必要があります。この情報はレンダリングに使用されるため、stateに保持します。

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

ユーザが「開始」を押すと、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>
    </>
  );
}

「停止」ボタンが押されたら、既存のインターバルをキャンセルして、now state変数の更新を停止する必要があります。clearIntervalを呼び出すことでこれを行うことができますが、ユーザが「開始」を押したときに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を使用する方が効率的な場合があります。

refsとstateの違い

refsは、stateほど「厳密」ではないように思えるかもしれません。例えば、常にstate設定関数を使用する必要はなく、値を変更できます。しかし、ほとんどの場合、stateを使用することをお勧めします。refsは、あまり必要としない「脱出ハッチ」です。stateとrefsの比較は以下のとおりです。

refsstate
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 の組み込みバージョンを提供しています。しかし、セッターのない通常のstate変数と考えることができます。オブジェクト指向プログラミングに慣れている場合、refsはインスタンスフィールドを思い起こさせるかもしれません。ただし、this.something の代わりに somethingRef.current と記述します。

refsを使用する場合

通常、コンポーネントがReactの「外部」に移動して外部API(多くの場合、コンポーネントの外観に影響を与えないブラウザAPI)と通信する必要がある場合にrefを使用します。以下に、これらのまれな状況をいくつか示します。

コンポーネントが何らかの値を保存する必要があるが、レンダリングロジックに影響を与えない場合は、refを選択します。

refsのベストプラクティス

これらの原則に従うと、コンポーネントがより予測可能になります。

  • refsを脱出ハッチとして扱います。refsは、外部システムまたはブラウザAPIを操作する場合に役立ちます。アプリケーションのロジックとデータフローの多くがrefsに依存している場合は、アプローチを再考する必要があるかもしれません。
  • レンダリング中に ref.current を読み書きしないでください。レンダリング中に何らかの情報が必要な場合は、代わりにstateを使用します。Reactは ref.current がいつ変更されるかわからないため、レンダリング中に読み取るだけでもコンポーネントの動作を予測するのが難しくなります。(これに対する唯一の例外は、if (!ref.current) ref.current = new Thing() のようなコードであり、最初のレンダリング中にのみrefを一度設定します。)

React stateの制限はrefsには適用されません。たとえば、stateはすべてのレンダリングのスナップショットのように動作し、同期的に更新されません。しかし、refの現在の値を変更すると、すぐに変更されます。

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

これは、ref自体が通常のJavaScriptオブジェクトであるため、そのように動作するためです。

また、refを操作するときに、変更を避けることを心配する必要もありません。変更しているオブジェクトがレンダリングに使用されていない限り、Reactはrefまたはその内容に対して何を行うかを気にしません。

RefsとDOM

refは任意の値に設定できます。しかし、refの最も一般的な使用例は、DOM要素にアクセスすることです。たとえば、プログラムで入力にフォーカスしたい場合に便利です。JSXのref属性にrefを渡すと(例:<div ref={myRef}>)、Reactは対応するDOM要素をmyRef.currentに格納します。要素がDOMから削除されると、ReactはmyRef.currentnullに更新します。詳細については、RefsによるDOM操作をご覧ください。

まとめ

  • refは、レンダリングに使用されない値を保持するための抜け穴です。頻繁に必要となるものではありません。
  • refは、currentという単一のプロパティを持つプレーンなJavaScriptオブジェクトであり、読み取りまたは設定が可能です。
  • Reactにrefを要求するには、useRefフックを呼び出します。
  • stateと同様に、refを使用すると、コンポーネントの再レンダリング間で情報を保持できます。
  • stateとは異なり、refのcurrent値を設定しても、再レンダリングはトリガーされません。
  • レンダリング中にref.currentを読み書きしないでください。これは、コンポーネントの予測を困難にします。

チャレンジ 1 4:
壊れたチャット入力を修正する

メッセージを入力して「送信」をクリックします。「送信完了!」というアラートが表示されるまでに3秒の遅延があることに気づくでしょう。この遅延中に、「元に戻す」ボタンが表示されます。それをクリックしてください。この「元に戻す」ボタンは、「送信完了!」メッセージが表示されないようにするためのものです。これは、handleSend中に保存されたタイムアウトIDに対してclearTimeoutを呼び出すことで行われます。しかし、「元に戻す」をクリックした後でも、「送信完了!」メッセージは表示されます。その理由を見つけて、修正してください。

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>
      }
    </>
  );
}