概要
Effect Hooksを使用することで、関数コンポーネント内で副作用を実行することができるようになります。
副作用とは、データ取得や更新、コンポーネント内のDOMの変更といったものが当てはまり、「関数の外に影響してしまうもの」です。
Effectsは、Reactパラダイムからの「逃げ道」なので、外部システムとやり取りがない場合は必要ないかもしれません。
useEffect
useEffectを使用した例
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
- 外部に接続するための
connect()
関数処理(セットアップコード)を渡していて、返り値は切断するためのdisconnect()
(クリーンアップコード)をセットしています。 - 第2引数には依存配列として、中の関数内で使用される値を入れます。
セットアップコードは、コンポーネントがページにマウントされた時に実行されます。また、依存配列の中身が変更された際にも実行されます。そして、アンマウントされた際に、クリーンアップコードが実行されます。
バグを発見しやすくするために、開発では実際のセットアップの前に、Reactがセットアップとクリーンアップを1回余計に実行します。これは、Effectのロジックが正しく実装されているかどうかを確認するためのストレステストです。もしこれが目に見える問題を引き起こすなら、クリーンアップ関数に何らかのロジックが欠けていることになります。クリーンアップ関数は、セットアップ関数が行っていたことを停止するか、元に戻すべきであるという理論に基づいています。
以下のsandboxでは開発モードで動作しているため、consoleで初期描画の際に余計にuseEffectの処理をやっていることがわかります。
カスタムフック
useEffectはカスタムフックとして、別ファイルに定義もできます。
以下のように関数としてラップして、再利用でき、ロジックをテストしやすくできます。
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
宣言した関数は以下のようにコンポーネント内で利用できます。
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
サードパーティなReactのコンポーネントでないウィジェットのコントロール
useEffectを使用して、サードパーティ製のウィジェットの状態をuseEffectで変更したりもできます。
Effectでのデータ取得の代替
useEffect内部でデータ取得をすることは一般的な方法ですが、サーバー上で実行されるわけではないので、クライアント側でJavaScriptをダウンロードしてアプリをレンダリングし、Fetchによるデータ取得をする必要がありあまり効率的ではないかもしれません。また、コンポーネントがアンマウントして再度マウントした場合にデータを取得し直す必要があります。
ReactQuery, useSWR等でクライアント再度キャッシュを検討する必要もあるかもしれません。
依存配列の指定
依存配列に指定すべきリアクティブな値には、propsやコンポーネント内部で直接宣言されているすべての変数や関数が含まれます。
例えば、以下の例では roomId
はprops、sereverUrl
はコンポーネント内で宣言されたuseStateによる変数のためにどちらもリアクティブな値となります。
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}
もし、serverUrlが依存関係でない(固定の値である)場合にはリアクティブな値でない場所で宣言してあげる必要があります。
以下のようにコンポーネント外で宣言することによって、依存配列から取り除くことが可能です。
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Effect時に前の状態に基づいて状態を更新したい場合
以下のような関数だと、count
がsetCount
によって更新されたタイミングでクリーンアップ関数が呼ばれ、セットアップコードが再度やり直しになるため微妙な動きです。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}
以下のTipsを使用すれば、これが解決できます。
つまり、 a => a + 1
のようなアップデートカンスを渡してあげることで、保留状態を受け取り次の計算をしてくれます。
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ Pass a state updater
}, 1000);
return () => clearInterval(intervalId);
}, []); // ✅ Now count is not a dependency
return <h1>{count}</h1>;
}
不要なオブジェクトや関数依存を削除する
以下のように、useEffectで使用しているオブジェクトや関数をコンポーネントの内部のトップレベルな箇所に書いてしまうと、このコンポーネントがレンダリングされるたびに、useEffectが実行されてしまいます。
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
なので、useEffect内でオブジェクトや関数を宣言していて依存しないようにしましょう。
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
useLayoutEffect
useEffectとほぼ同じ機能を持ちますが、useLayoutEffectは同期的に呼び出されます。なのでコンポーネントがマウントされる前のタイミングで発火します。
同期的に発火するので、コンポーネント自体の処理が止まってしまうため useEffectで事足りる場合は使用しないほうが良いようです。
useInsertionEffect
useInsertionEffectは、CSS-in-JSライブラリの作成者のためのフックです。
ここでは割愛します。
参考