⚙️
React で初期化時に 1 回だけ処理を実行したいときの書き方
現時点で React でクラス コンポーネントを書くことは稀になったと思いますが、クラス コンポーネントあったコンポーネントのマウント時に発生する componentDidMount
は関数コンポーネントではなくなりました。ではどうやるかというと useEffect
を使います。
function HelloWorldComponent() {
// これはコンポーネントの初期化時にのみ実行される
// 依存関係に空の配列を指定する必要がある
React.useEffect(() => {
console.log('Hello world!');
}, []);
return (
<div>Hello world!</div>
);
}
実際には初期化時には外部の API やデータ ストアへのアクセスといったロジックが含まれます。これらはカスタム フックなどを経由して実行されることが多いです。試しに上記のコードの console.log()
を何かしらのサービスの呼び出しに書き換えてみます。
function HelloWorldComponent() {
// 何かしらのサービスを提供するカスタム フック
const someService = useSomeService();
// コンポーネントの初期化時にのみサービスを呼び出したい
React.useEffect(() => {
someService.run('Hello world!');
}, []);
return (
<div>Hello world!</div>
);
}
これは動作するのですが ESLint のルール (react-hooks/exhaustive-deps
) に引っ掛かります。依存関係を指定していないからですね。なので修正します。
function HelloWorldComponent() {
// 何かしらのサービスを提供するカスタム フック
const someService = useSomeService();
// コンポーネントの初期化時にのみサービスを呼び出したい
// ESLint に怒られないように依存関係を追加する
React.useEffect(() => {
someService.run('Hello world!');
}, [ someService ]);
return (
<div>Hello world!</div>
);
}
おそらくこれでほとんどの場合は期待通りに動作します。ただしそれは someService
の値が不変である前提においてです。こうした依存関係がある変数の値の意図しない更新によって useEffect
が複数回呼ばれてしまうということはたまによくあることです。このパターンの場合はカスタム フックの作りが悪いのでそちらを直すべきではあるのですが、呼び出し側で対応が必要な場合もあります。そこで、複数回の呼び出しを禁止するためにフラグを用意します。
function HelloWorldComponent() {
// 初期化を検知するフラグ
const [ loading, setLoading ] = React.useState(true);
// 何かしらのサービスを提供するカスタム フック
const someService = useSomeService();
// コンポーネントの初期化時にのみサービスを呼び出したい
React.useEffect(() => {
// すでに初期化されていたら処理を抜ける
if (!loading) {
return;
}
someService.run('Hello world!');
// 初期化済みのフラグを立てる
setLoading(false);
}, [ loading, someService ]);
return (
<div>Hello world!</div>
);
}
これで初期化時に 1 回のみ実行されるようにすることができます。
正しく動いているように見えるけど開発者ツールで除くと同じ API を 2 回呼び出していることがある、というのは本当によくあるあるなので、意識して実装していきたいものですね。
Discussion