Reactは、レンダー出力に合わせてDOMを自動的に更新するため、コンポーネントでDOMを操作する必要はほとんどありません。ただし、ノードにフォーカスしたり、ノードまでスクロールしたり、ノードのサイズや位置を計測したりするために、Reactが管理するDOM要素にアクセスする必要がある場合があります。Reactにはこれらのことを行うための組み込みの方法がないため、DOMノードへのrefが必要になります。

以下を学びます

  • ref属性を使って、Reactが管理するDOMノードにアクセスする方法
  • ref JSX属性とuseRefフックとの関係
  • 別のコンポーネントのDOMノードにアクセスする方法
  • Reactが管理するDOMを変更しても安全な場合

ノードへのrefを取得する

Reactが管理するDOMノードにアクセスするには、まず、useRefフックをインポートします

import { useRef } from 'react';

次に、それを使ってコンポーネント内でrefを宣言します

const myRef = useRef(null);

最後に、DOMノードを取得したいJSXタグに、refをref属性として渡します

<div ref={myRef}>

useRefフックは、currentという名前の単一のプロパティを持つオブジェクトを返します。最初は、myRef.currentnullになります。Reactがこの<div>のDOMノードを作成すると、Reactはこのノードへの参照をmyRef.currentに格納します。その後、イベントハンドラからこのDOMノードにアクセスし、定義されている組み込みのブラウザAPIを使用できます。

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

例:テキスト入力にフォーカスを当てる

この例では、ボタンをクリックすると入力にフォーカスが当たります

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

これを実装するには

  1. inputRefuseRefフックで宣言します。
  2. <input ref={inputRef}>として渡します。これにより、Reactはこの<input>のDOMノードをinputRef.currentに格納するように指示されます。
  3. handleClick関数で、inputRef.currentから入力DOMノードを読み取り、focus()inputRef.current.focus()で呼び出します。
  4. handleClickイベントハンドラを<button>onClickで渡します。

DOM操作がrefの最も一般的なユースケースですが、useRefフックは、タイマーIDのように、Reactの外部のものを保存するためにも使用できます。ステートと同様に、refはレンダリング間で保持されます。refは、設定しても再レンダリングをトリガーしないステート変数のようなものです。refについては、「Refsによる値の参照」をご覧ください。

例:要素へのスクロール

コンポーネント内で複数のrefを持つことができます。この例では、3つの画像からなるカルーセルがあります。各ボタンは、対応するDOMノードに対してブラウザのscrollIntoView()メソッドを呼び出すことで、画像を中央に表示します。

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToSecondCat() {
    secondCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToThirdCat() {
    thirdCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Neo
        </button>
        <button onClick={handleScrollToSecondCat}>
          Millie
        </button>
        <button onClick={handleScrollToThirdCat}>
          Bella
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placecats.com/neo/300/200"
              alt="Neo"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/millie/200/200"
              alt="Millie"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placecats.com/bella/199/200"
              alt="Bella"
              ref={thirdCatRef}
            />
          </li>
        </ul>
      </div>
    </>
  );
}

詳細

refコールバックを使用してrefのリストを管理する方法

上記の例では、定義済みの数のrefがあります。ただし、リスト内の各アイテムへのrefが必要になる場合があり、いくつになるかわからない場合があります。次のようなものは機能しません

<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>

これは、フックはコンポーネントのトップレベルでのみ呼び出す必要があるためです。useRefをループ内、条件内、またはmap()呼び出し内で呼び出すことはできません。

これを回避する1つの可能な方法は、親要素への単一のrefを取得し、次にquerySelectorAllのようなDOM操作メソッドを使用して、そこから個々の子ノードを「見つける」ことです。ただし、これは脆弱であり、DOM構造が変更された場合に壊れる可能性があります。

別の解決策は、ref属性に関数を渡すことです。これは、refコールバックと呼ばれます。Reactは、refを設定するタイミングでDOMノードを使用してrefコールバックを呼び出し、クリアするタイミングでnullを使用して呼び出します。これにより、独自の配列またはMapを維持し、インデックスまたは何らかのIDでrefにアクセスできます。

この例では、このアプローチを使用して長いリスト内の任意のノードにスクロールする方法を示します。

import { useRef, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    node.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Neo</button>
        <button onClick={() => scrollToCat(catList[5])}>Millie</button>
        <button onClick={() => scrollToCat(catList[9])}>Bella</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                map.set(cat, node);

                return () => {
                  map.delete(cat);
                };
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

この例では、itemsRefは単一のDOMノードを保持していません。代わりに、アイテムIDからDOMノードへのMapを保持します。(refは任意の値を保持できます!)リスト内のすべてのアイテムのrefコールバックは、Mapを更新するように注意します。

<li
key={cat.id}
ref={node => {
const map = getMap();
// Add to the Map
map.set(cat, node);

return () => {
// Remove from the Map
map.delete(cat);
};
}}
>

これにより、後でMapから個々のDOMノードを読み取ることができます。

注意

厳格モードが有効になっている場合、開発環境ではrefコールバックが2回実行されます。

コールバックrefでこれがバグを見つけるのにどのように役立つかについて詳しく読んでください。

別のコンポーネントのDOMノードへのアクセス

<input />のようなブラウザ要素を出力する組み込みコンポーネントにrefを配置すると、Reactはそのrefのcurrentプロパティを対応するDOMノード(ブラウザの実際の<input />など)に設定します。

ただし、<MyInput />のような独自のコンポーネントにrefを配置しようとすると、デフォルトではnullが返されます。以下に、それを実証する例を示します。ボタンをクリックしても、入力がフォーカスされないことに注意してください。

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

問題を認識できるように、Reactはコンソールにもエラーを出力します。

コンソール
警告:関数コンポーネントにrefを付与することはできません。このrefにアクセスしようとすると失敗します。React.forwardRef()を使用する予定でしたか?

これは、デフォルトではReactがコンポーネントに他のコンポーネントのDOMノードへのアクセスを許可しないためです。自分自身の子の場合でもです!これは意図的なものです。refは、控えめに使用する必要がある脱出ハッチです。別のコンポーネントのDOMノードを手動で操作すると、コードがさらに脆弱になります。

代わりに、DOMノードを公開したいコンポーネントは、その動作をオプトインする必要があります。コンポーネントは、そのrefを子の一つに「転送」することを指定できます。これがMyInputforwardRef APIを使用する方法です。

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

これがその仕組みです。

  1. <MyInput ref={inputRef} /> は、対応するDOMノードを inputRef.current に格納するようReactに指示します。ただし、これは MyInput コンポーネントがそれを受け入れるかどうかによります。デフォルトでは受け入れません。
  2. MyInput コンポーネントは forwardRef を使用して宣言されています。これにより、上記からの inputRef を2番目の ref 引数として受け取ることを選択します。これは props の後に宣言されます。
  3. MyInput 自体は、受け取った ref を内部の <input> に渡します。

これで、ボタンをクリックして入力にフォーカスできるようになります。

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

デザインシステムでは、ボタン、入力などの低レベルコンポーネントが、そのrefをDOMノードに転送するのが一般的なパターンです。一方、フォーム、リスト、ページセクションなどの高レベルコンポーネントは、DOM構造への偶発的な依存関係を避けるために、通常はDOMノードを公開しません。

詳細

命令型ハンドルでAPIのサブセットを公開する

上記の例では、MyInput は元のDOM入力要素を公開しています。これにより、親コンポーネントはそれに focus() を呼び出すことができます。ただし、これにより、親コンポーネントが他のこと(たとえば、CSSスタイルを変更するなど)を実行することもできます。まれなケースでは、公開される機能を制限したい場合があります。これは、useImperativeHandle を使用して行うことができます。

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

ここで、MyInput 内の realInputRef は、実際の入力DOMノードを保持します。ただし、useImperativeHandle は、親コンポーネントへのrefの値として、独自の特別なオブジェクトを提供するようにReactに指示します。したがって、Form コンポーネント内の inputRef.currentfocus メソッドのみを持ちます。この場合、refの「ハンドル」はDOMノードではなく、useImperativeHandle 呼び出し内で作成するカスタムオブジェクトです。

Reactがrefをアタッチするとき

Reactでは、すべての更新が2つのフェーズに分割されます。

  • レンダー中、Reactは画面に何を表示すべきかを把握するためにコンポーネントを呼び出します。
  • コミット中、ReactはDOMに変更を適用します。

一般に、レンダリング中にrefにアクセスすることは推奨されません。これはDOMノードを保持するrefにも当てはまります。最初のレンダリング中、DOMノードはまだ作成されていないため、ref.currentnull になります。また、更新のレンダリング中、DOMノードはまだ更新されていません。したがって、それらを読み取るには時期尚早です。

Reactはコミット中にref.currentを設定します。DOMを更新する前に、Reactは影響を受けるref.current値をnullに設定します。DOMを更新した後、Reactはそれらを対応するDOMノードにすぐに設定します。

通常、イベントハンドラーからrefにアクセスします。 refを使用して何かを実行したいが、実行する特定のイベントがない場合は、Effectが必要になる場合があります。次のページでEffectについて説明します。

詳細

flushSyncで状態の更新を同期的にフラッシュする

新しいtodoを追加し、画面をリストの最後の子まで下にスクロールするようなコードを考えてみましょう。なぜか、最後に追加されたtodoの直前のtodoまでしかスクロールされないことに気づくでしょう。

import { useState, useRef } from 'react';

export default function TodoList() {
  const listRef = useRef(null);
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAdd() {
    const newTodo = { id: nextId++, text: text };
    setText('');
    setTodos([ ...todos, newTodo]);
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  return (
    <>
      <button onClick={handleAdd}>
        Add
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <ul ref={listRef}>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
  initialTodos.push({
    id: nextId++,
    text: 'Todo #' + (i + 1)
  });
}

問題は、これらの2行にあります。

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

Reactでは、状態の更新はキューに入れられます。 通常、これは望ましい動作です。ただし、ここでは setTodos がDOMをすぐに更新しないため、問題が発生します。そのため、リストを最後の要素にスクロールする時点で、todoはまだ追加されていません。これが、スクロールが常に1項目「遅れる」理由です。

この問題を修正するには、ReactにDOMを同期的に更新(「フラッシュ」)させるように強制できます。これを行うには、react-dom から flushSync をインポートし、状態の更新を flushSync 呼び出しでラップします。

flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

これにより、Reactは、flushSync でラップされたコードが実行された直後に、同期的にDOMを更新するように指示されます。その結果、最後のtodoは、それをスクロールしようとするまでに既にDOMに存在することになります。

import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';

export default function TodoList() {
  const listRef = useRef(null);
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAdd() {
    const newTodo = { id: nextId++, text: text };
    flushSync(() => {
      setText('');
      setTodos([ ...todos, newTodo]);      
    });
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  return (
    <>
      <button onClick={handleAdd}>
        Add
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <ul ref={listRef}>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
  initialTodos.push({
    id: nextId++,
    text: 'Todo #' + (i + 1)
  });
}

refsを使ったDOM操作のベストプラクティス

Refsは緊急脱出用のハッチです。「Reactの外部に踏み出す」必要がある場合にのみ使用すべきです。一般的な例としては、フォーカスの管理、スクロール位置の管理、またはReactが公開していないブラウザAPIの呼び出しなどがあります。

フォーカスやスクロールのような非破壊的なアクションに固執していれば、問題に遭遇することはないでしょう。しかし、手動でDOMを変更しようとすると、Reactが行っている変更と競合するリスクがあります。

この問題を説明するために、この例にはウェルカムメッセージと2つのボタンが含まれています。最初のボタンは、条件付きレンダリング状態を使用して、その存在を切り替えます。これは通常Reactで行う方法です。2番目のボタンは、remove() DOM APIを使用して、Reactの制御外から強制的にDOMから削除します。

「setStateで切り替え」を数回押してみてください。メッセージは消えたり、再び表示されたりするはずです。次に「DOMから削除」を押してください。これにより、強制的に削除されます。最後に「setStateで切り替え」を押してください。

import { useState, useRef } from 'react';

export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}>
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

DOM要素を手動で削除した後、setStateを使って再び表示しようとすると、クラッシュが発生します。これは、DOMを変更してしまい、Reactがどのように管理を続けるべきか分からなくなるためです。

Reactによって管理されているDOMノードの変更は避けてください。Reactによって管理されている要素に対して、変更、子要素の追加、子要素の削除を行うと、視覚的な結果の不一致や上記のようなクラッシュが発生する可能性があります。

ただし、これは絶対にできないという意味ではありません。注意が必要です。Reactが更新する理由がないDOMの部分は安全に変更できます。たとえば、いくつかの<div>がJSXで常に空の場合、Reactがその子リストに触れる理由はありません。したがって、そこに要素を手動で追加または削除しても安全です。

まとめ

  • refsは一般的な概念ですが、ほとんどの場合、DOM要素を保持するために使用します。
  • ReactにmyRef.currentにDOMノードを配置するように指示するには、<div ref={myRef}>を渡します。
  • 通常、refsは、フォーカス、スクロール、またはDOM要素の測定などの非破壊的なアクションに使用します。
  • コンポーネントは、デフォルトではDOMノードを公開しません。 forwardRefを使用し、2番目のref引数を特定のノードに渡すことで、DOMノードを公開するように選択できます。
  • Reactによって管理されているDOMノードの変更は避けてください。
  • Reactによって管理されているDOMノードを変更する場合は、Reactが更新する理由がない部分を変更してください。

チャレンジ 1 4:
ビデオを再生および一時停止する

この例では、ボタンが状態変数を切り替えて、再生状態と一時停止状態を切り替えます。ただし、実際にビデオを再生または一時停止するには、状態を切り替えるだけでは十分ではありません。<video>のDOM要素でplay()およびpause()を呼び出す必要もあります。refを追加して、ボタンが機能するようにしてください。

import { useState, useRef } from 'react';

export default function VideoPlayer() {
  const [isPlaying, setIsPlaying] = useState(false);

  function handleClick() {
    const nextIsPlaying = !isPlaying;
    setIsPlaying(nextIsPlaying);
  }

  return (
    <>
      <button onClick={handleClick}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <video width="250">
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  )
}

追加の課題として、ユーザーがビデオを右クリックして、組み込みのブラウザメディアコントロールを使用してビデオを再生した場合でも、「再生」ボタンをビデオが再生中かどうかと同期させてください。これを行うには、ビデオのonPlayonPauseをリッスンするとよいでしょう。