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 の呼び出し回数を確認すると、意図しない挙動に気付くことがあります。 React の useEffect では依存関係やカスタムフックの設計に注意し、必要に応じてフラグなどで制御することが重要です。
Discussion