コンポーネントによっては、React 外のシステムに対して制御や同期を必要とする場合があります。例えば、ブラウザ API を使用して入力フィールドにフォーカスを当てる、React を使用せずに実装されたビデオプレーヤの再生や一時停止を行う、あるいはリモートサーバに接続してメッセージをリッスンする、といったものです。この章では、React の「外側に踏み出して」外部システムに接続するための避難ハッチ (escape hatch) を学びます。アプリケーションのロジックとデータフローの大部分は、これらの機能に依存しないようにすべきです。
この章で学ぶこと
ref で値を参照する
コンポーネントに情報を「記憶」させたいが、その情報が新しいレンダーをトリガしないようにしたい場合、ref を使うことができます。
const ref = useRef(0);
state と同様に、ref は React によって再レンダー間で保持されます。しかし、state はセットするとコンポーネントが再レンダーされます。ref は、変更してもコンポーネントが再レンダーされません! ref.current
プロパティを通じて ref の現在の値にアクセスできます。
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 は、React が管理しない、コンポーネントの秘密のポケットのようなものです。例えば、タイムアウト ID、DOM 要素、その他コンポーネントのレンダー出力に影響を与えないオブジェクトを格納するために ref を使用できます。
ref で DOM を操作する
React はレンダー結果に合致するよう自動的に DOM を更新するため、コンポーネントで DOM を操作する必要は通常ほとんどありません。ただし、ノードにフォーカスを当てたり、スクロールさせたり、サイズや位置を測定したりするなどの場合に、React が管理する DOM 要素へのアクセスが必要なことがあります。React にはこれらを行う組み込みの方法が存在しないため、DOM ノードを参照する ref が必要になります。例えば、以下のボタンをクリックすると、ref を使用して入力欄にフォーカスが当たります。
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> </> ); }
エフェクトを使って同期を行う
一部のコンポーネントは外部システムと同期を行う必要があります。例えば、React の state に基づいて非 React 製コンポーネントを制御したり、サーバとの接続を確立したり、コンポーネントが画面に表示されたときに分析用のログを送信したりしたいかもしれません。特定のイベントを処理するイベントハンドラとは異なり、エフェクト (Effect) を使うことで、レンダー後にコードを実行することができます。これは React 外のシステムとコンポーネントを同期させるために使用します。
Play/Pause を何度か押して、ビデオプレーヤが isPlaying
の値と同期していることを確認してみてください。
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
多くのエフェクトは自身を「クリーンアップ」します。例えば、チャットサーバへの接続をセットアップするエフェクトは、そのサーバからコンポーネントを切断する方法を React に伝えるためにクリーンアップ関数を返します。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
開発環境では、React はエフェクトの実行とクリーンアップを素早く 1 回余分に実行します。これが "✅ Connecting..."
が 2 回表示される理由です。これにより、クリーンアップ関数の実装を忘れることがないようになっています。
そのエフェクトは不要かも
エフェクトは React のパラダイムからの避難ハッチです。React の外に「踏み出して」、何らかの外部システムと同期させることができるものです。外部システムが関与していない場合(例えば、props や state の変更に合わせてコンポーネントの state を更新したい場合)、エフェクトは必要ありません。不要なエフェクトを削除することで、コードが読みやすくなり、実行速度が向上し、エラーが発生しにくくなります。
エフェクトが不要な場合として一般的なのは次の 2 つのケースです。
- レンダーのためのデータ変換にエフェクトは必要ありません。
- ユーザイベントの処理にエフェクトは必要ありません。
例えば、他の state に基づいてほかの state を調整するのに、エフェクトは必要ありません。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
代わりに、レンダー時にできるだけ多くを計算するようにします。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
ただし、外部システムと同期するためにはエフェクトが必要です。
リアクティブなエフェクトのライフサイクル
エフェクトはコンポーネントとは異なるライフサイクルを持ちます。コンポーネントは、マウント、更新、アンマウントを行うことができます。エフェクトは 2 つのことしかできません。同期の開始と、同期の停止です。エフェクトが props や state に依存し、これらが時間と共に変化する場合、このサイクルは繰り返し発生します。
以下のエフェクトは props である roomId
に依存しています。props はリアクティブな値 (reactive value)、つまり再レンダー時に変わる可能性がある値です。roomId
が変更されると、エフェクトが再同期(サーバに再接続)されていることに注目してください。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
React は、エフェクトの依存配列が正しく指定されているかをチェックするためのリンタルールを提供しています。上記の例で roomId
を依存値のリストに指定し忘れた場合、リンタがそのバグを自動的に見つけてくれます。
Ready to learn this topic?
リアクティブなエフェクトのライフサイクルを読んで、エフェクトのライフサイクルがコンポーネントのライフサイクルとどのように異なるかを学びましょう。
Read Moreイベントとエフェクトを切り離す
イベントハンドラは同じインタラクションを再度実行した場合のみ再実行されます。エフェクトはイベントハンドラとは異なり、props や state 変数のようなそれが読み取る値が前回のレンダー時の値と異なる場合に再同期を行います。また、ある値には反応して再実行するが、他の値には反応しないエフェクトなど、両方の動作をミックスさせたい場合もあります。
エフェクト内のすべてのコードはリアクティブです。それが読み取るリアクティブな値が再レンダーにより変更された場合、再度実行されます。例えば、このエフェクトは roomId
と theme
のいずれかが変更された場合にチャットに再接続します。
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
これはあまり良くありません。roomId
が変更された場合にのみチャットに再接続したいのです。theme
の切り替えでチャットの再接続が起きるべきではありません! theme
を読み取るコードをエフェクトからエフェクトイベント (Effect Event) に移動させましょう。
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
エフェクトイベント内のコードはリアクティブではないため、theme
を変更してもエフェクトが再接続されることはありません。
エフェクトから依存値を取り除く
エフェクトを記述する際、リンタはエフェクトが読み取るすべてのリアクティブな値(props や state など)がエフェクトの依存値のリストに含まれているか確認します。これにより、エフェクトがコンポーネントの最新の props や state と同期された状態を保つことができます。不要な依存値があると、エフェクトが頻繁に実行され過ぎたり、無限ループが発生したりすることがあります。不要な依存値を取り除く方法はケースによって異なります。
たとえば、このエフェクトは入力フィールドを編集するたびに再作成される options
オブジェクトに依存しています。
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const options = { serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
チャットでメッセージを入力し始めるたびにチャットが再接続されることは望ましくありません。この問題を解決するために、options
オブジェクトの作成をエフェクト内に移動し、エフェクトが roomId
文字列にのみ依存するようにします:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
依存値のリストを編集して options
を削除することから始めなかったことに注意してください。それは間違いです。代わりに、依存関係が不要になるよう、周囲のコードを変更したのです。依存値のリストは、あなたのエフェクトのコードが使用しているすべてのリアクティブな値の一覧だと考えてください。そのリストに何を置くかを意識して選ぶのではありません。リストはあなたのコードの説明にすぎません。依存配列を変更するには、コードの方を変更します。
カスタムフックでロジックを再利用する
React には useState
、useContext
、useEffect
など複数の組み込みフックが存在します。しかし、データの取得やユーザのオンライン状態の監視、チャットルームへの接続など、より特化した目的のためのフックが欲しいこともあります。これらを行うためには、アプリケーションの要求に合わせて独自のフックを作成することが可能です。
以下の例では、usePointerPosition
カスタムフックがカーソルの位置を追跡し、useDelayedValue
カスタムフックが値をある一定のミリ秒だけ「遅らせて」返します。サンドボックスのプレビューエリア上でカーソルを動かすと、カーソルに追従する複数のドットによる軌跡が現れます。
import { usePointerPosition } from './usePointerPosition.js'; import { useDelayedValue } from './useDelayedValue.js'; export default function Canvas() { const pos1 = usePointerPosition(); const pos2 = useDelayedValue(pos1, 100); const pos3 = useDelayedValue(pos2, 200); const pos4 = useDelayedValue(pos3, 100); const pos5 = useDelayedValue(pos4, 50); return ( <> <Dot position={pos1} opacity={1} /> <Dot position={pos2} opacity={0.8} /> <Dot position={pos3} opacity={0.6} /> <Dot position={pos4} opacity={0.4} /> <Dot position={pos5} opacity={0.2} /> </> ); } function Dot({ position, opacity }) { return ( <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> ); }
カスタムフックを作成し、それらを組み合わせ、データを受け渡し、コンポーネント間で再利用することができます。アプリが成長するにつれて、すでに書いたカスタムフックを再利用することで、手作業でエフェクトを書く回数が減っていきます。また、React コミュニティによってメンテナンスされている優れたカスタムフックも多数あります。
次のステップ
ref で値を参照するに進み、この章をページごとに読み進めてください!