コンポーネントの中には、React の外部にあるシステムを制御および同期する必要があるものがあります。たとえば、ブラウザ API を使用して入力をフォーカスしたり、React なしで実装されたビデオプレーヤーを再生および一時停止したり、リモートサーバーからのメッセージに接続してリッスンしたりする必要がある場合があります。この章では、React の「外側」に stepping out し、外部システムに接続できるエスケープハッチについて説明します。アプリケーションロジックとデータフローのほとんどは、これらの機能に依存しないようにする必要があります。
この章の内容
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 が追跡しないコンポーネントの秘密のポケットのようなものです。たとえば、ref を使用して、タイムアウト ID、DOM 要素、およびコンポーネントのレンダリング出力に影響を与えないその他のオブジェクトを格納できます。
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> </> ); }
Effect による同期
一部のコンポーネントは、外部システムと同期する必要があります。たとえば、React の state に基づいて React 以外のコンポーネントを制御したり、サーバー接続をセットアップしたり、コンポーネントが画面に表示されたときに分析ログを送信したりする場合があります。特定のイベントを処理できるイベントハンドラとは異なり、_Effect_を使用すると、レンダリング後にコードを実行できます。 Effect を使用して、コンポーネントを React の外部にあるシステムと同期させます。
再生/一時停止を数回押して、ビデオプレーヤーが isPlaying
prop の値と同期していることを確認してください。
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" /> </> ); }
多くの Effect は、自身を「クリーンアップ」します。たとえば、チャットサーバーへの接続をセットアップする Effect は、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 は Effect をすぐに実行し、もう1回クリーンアップします。このため、"✅ Connecting..."
が2回出力されます。これにより、クリーンアップ関数の実装を忘れないようにします。
Effect が不要な場合
Effect は、React パラダイムからのエスケープハッチです。 Effect を使用すると、React の「外側」に stepping out し、コンポーネントを外部システムと同期させることができます。外部システムが関係ない場合(たとえば、props または state が変更されたときにコンポーネントの state を更新する場合)、Effect は必要ありません。不要な Effect を削除すると、コードが読みやすくなり、実行速度が向上し、エラーが発生しにくくなります。
Effect が不要な一般的なケースは2つあります。
- レンダリング用のデータを変換するために Effect は必要ありません。
- ユーザーイベントを処理するために Effect は必要ありません。
たとえば、他の状態に基づいて状態を調整するために Effect は必要ありません。
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;
// ...
}
ただし、外部システムと同期するには、Effect が必要です。
リアクティブ Effect のライフサイクル
Effect はコンポーネントとは異なるライフサイクルを持ちます。コンポーネントはマウント、更新、またはアンマウントされる可能性があります。Effect は、何かの同期を開始し、後で同期を停止するという 2 つのことだけを実行できます。Effect が時間の経過とともに変化する props や状態に依存している場合、このサイクルは複数回発生する可能性があります。
この Effect は roomId
prop の値に依存しています。Props はリアクティブ値であり、再レンダリング時に変更される可能性があります。roomId
が変更された場合、Effect が再同期(およびサーバーへの再接続)されることに注意してください。
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 は、Effect の依存関係を正しく指定したかどうかを確認するためのリンタールールを提供しています。上記の例で依存関係のリストに roomId
を指定するのを忘れた場合、リンターは自動的にそのバグを見つけます。
このトピックを学ぶ準備はできましたか?
リアクティブイベントのライフサイクル を読んで、Effect のライフサイクルがコンポーネントのライフサイクルとどのように異なるかについて学びましょう。
続きを読むイベントと Effect の分離 (以下略、SVGは同じなので省略)
イベントハンドラは、同じインタラクションを再度実行した場合にのみ再実行されます。イベントハンドラとは異なり、Effect は、読み取る値(props や状態など)が前回のレンダリング時と異なる場合に再同期します。場合によっては、両方の動作を組み合わせたい場合があります。つまり、一部の値に応答して再実行されるが、他の値には応答しない Effect です。
Effect 内のすべてのコードはリアクティブです。再レンダリングによって読み取るリアクティブ値が変更された場合、再度実行されます。たとえば、この Effect は、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 からEffect イベントに移動します。
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'} /> </> ); }
Effect イベント内のコードはリアクティブではないため、theme
を変更しても Effect が再接続されなくなります。
Effect の依存関係の削除 (以下略、SVGは同じなので省略)
Effect を記述すると、リンターは、Effect が読み取るすべてのリアクティブ値(props や状態など)が Effect の依存関係のリストに含まれていることを確認します。これにより、Effect がコンポーネントの最新の props と状態と同期された状態に保たれます。不要な依存関係があると、Effect が頻繁に実行されたり、無限ループが発生したりする可能性があります。削除方法はケースによって異なります。
たとえば、この Effect は、入力を編集するたびに再作成される 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
オブジェクトの作成を Effect 内に移動して、Effect が 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
依存関係を削除することから始めなかったことに注意してください。それは間違っています。代わりに、周囲のコードを変更して、依存関係が不要になるようにしました。依存関係リストは、Effect のコードで使用されるすべてのリアクティブ値のリストと考えてください。そのリストに何を置くかを意図的に選択するわけではありません。リストはコードを記述しています。依存関係リストを変更するには、コードを変更します。
カスタム Hooks でロジックを再利用する (以下略、SVGは同じなので省略)
React には、useState
、useContext
、useEffect
などの組み込み Hooks が付属しています。場合によっては、より具体的な目的のための Hook があればいいのにと思うことがあります。たとえば、データを取得したり、ユーザーがオンラインかどうかを追跡したり、チャットルームに接続したりするためです。これを行うには、アプリケーションのニーズに合わせて独自の Hooks を作成できます。
この例では、usePointerPosition
カスタム Hook はカーソル位置を追跡し、useDelayedValue
カスタム Hook は、渡した値から特定のミリ秒数「遅れている」値を返します。サンドボックスプレビュー領域にカーソルを移動すると、カーソルを追跡する点の移動軌跡が表示されます。
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, }} /> ); }
カスタム Hooks を作成し、それらをまとめて構成し、それらの間でデータを渡し、コンポーネント間で再利用できます。アプリが成長するにつれて、すでに記述したカスタム Hooks を再利用できるため、手動で記述する Effect が少なくなります。React コミュニティによって維持されている優れたカスタム Hooks もたくさんあります。
次は? (以下略、SVGは同じなので省略)
Refs での値の参照 にアクセスして、この章を 1 ページずつ読み始めましょう!