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.current
はnull
になります。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> </> ); }
これを実装するには
inputRef
をuseRef
フックで宣言します。<input ref={inputRef}>
として渡します。これにより、Reactはこの<input>
のDOMノードをinputRef.current
に格納するように指示されます。handleClick
関数で、inputRef.current
から入力DOMノードを読み取り、focus()
をinputRef.current.focus()
で呼び出します。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が必要になる場合があり、いくつになるかわからない場合があります。次のようなものは機能しません。
<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ノードを読み取ることができます。
別のコンポーネントの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はコンソールにもエラーを出力します。
これは、デフォルトではReactがコンポーネントに他のコンポーネントのDOMノードへのアクセスを許可しないためです。自分自身の子の場合でもです!これは意図的なものです。refは、控えめに使用する必要がある脱出ハッチです。別のコンポーネントのDOMノードを手動で操作すると、コードがさらに脆弱になります。
代わりに、DOMノードを公開したいコンポーネントは、その動作をオプトインする必要があります。コンポーネントは、そのrefを子の一つに「転送」することを指定できます。これがMyInput
がforwardRef
APIを使用する方法です。
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
これがその仕組みです。
<MyInput ref={inputRef} />
は、対応するDOMノードをinputRef.current
に格納するようReactに指示します。ただし、これはMyInput
コンポーネントがそれを受け入れるかどうかによります。デフォルトでは受け入れません。MyInput
コンポーネントはforwardRef
を使用して宣言されています。これにより、上記からのinputRef
を2番目のref
引数として受け取ることを選択します。これはprops
の後に宣言されます。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ノードを公開しません。
詳細
上記の例では、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.current
は focus
メソッドのみを持ちます。この場合、refの「ハンドル」はDOMノードではなく、useImperativeHandle
呼び出し内で作成するカスタムオブジェクトです。
Reactがrefをアタッチするとき
Reactでは、すべての更新が2つのフェーズに分割されます。
- レンダー中、Reactは画面に何を表示すべきかを把握するためにコンポーネントを呼び出します。
- コミット中、ReactはDOMに変更を適用します。
一般に、レンダリング中にrefにアクセスすることは推奨されません。これはDOMノードを保持するrefにも当てはまります。最初のレンダリング中、DOMノードはまだ作成されていないため、ref.current
は null
になります。また、更新のレンダリング中、DOMノードはまだ更新されていません。したがって、それらを読み取るには時期尚早です。
Reactはコミット中にref.current
を設定します。DOMを更新する前に、Reactは影響を受けるref.current
値をnull
に設定します。DOMを更新した後、Reactはそれらを対応するDOMノードにすぐに設定します。
通常、イベントハンドラーからrefにアクセスします。 refを使用して何かを実行したいが、実行する特定のイベントがない場合は、Effectが必要になる場合があります。次のページでEffectについて説明します。
詳細
新しい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> </> ) }
追加の課題として、ユーザーがビデオを右クリックして、組み込みのブラウザメディアコントロールを使用してビデオを再生した場合でも、「再生」ボタンをビデオが再生中かどうかと同期させてください。これを行うには、ビデオのonPlay
とonPause
をリッスンするとよいでしょう。