【実務・中級編】 依存配列が空配列[]の場合の挙動 – React実践ガイド

フロントエンドの荒波を共闘する仲間たちよ、今日も今日とてコンポーネントのライフサイクルと泥臭く格闘しているかい?

実務でReactを触っていると、避けて通れないのが「副作用の制御」だ。中でも`useEffect`の依存配列(dependency array)に空配列 `[]` を突っ込むパターンは、ジュニアからシニアへの階段を登る中級エンジニアにとって、最初の大きな分かれ道になる。

今日は、この「空配列 `[]`」がReactの内部やブラウザの裏側でどう扱われているのか、そして現場でよくある「APIの初期取得」を例に、綺麗でバグを踏まないコードを書くための実践知を授けよう。

—

1. 依存配列が「空」の正体:マウント時実行の幻想と現実

まず、大前提として知っておいてほしい。公式ドキュメントには「マウント時に一度だけ実行される」と書かれていることが多いが、シニアの視点から言えば、これは半分正しくて半分危うい認識だ。

正確には、「前回のレンダリング時から依存している値が一切変化していない(そもそも監視する値がないため常に一致している)ため、Reactが初回レンダー後のコミットフェーズの終わりに副作用を一度だけ発火させる」というのが正確なメカニズムだ。

ブラウザの裏側で何が起きているか?

1. Render Phase(レンダーフェーズ): Reactがコンポーネントを呼び出し、JSXのツリーを構築する。この時点ではまだ画面は変わっていない。
2. Commit Phase(コミットフェーズ): ReactがDOMを実際にブラウザへ反映(ペイント)する。
3. Paint(ペイント): ブラウザが画面を描画し終える。
4. Effect Execution(副作用の実行): ブラウザの描画ブロックをブロックしないよう非同期的に、`useEffect` の中身(およびクリーンアップ)のスケジュール処理が走り、空配列であれば「初回のみ」の処理が実行される。

つまり、`[]` を渡すということは、「この副作用は、コンポーネント内のいかなるstateやpropsの変化にも依存しません。だから再実行の必要はありません」とReactに誓約書を差し出す行為なのだ。

—

2. 現場のユースケース:APIの初期データ取得におけるベストプラクティス

中級者によくあるアンチパターンとして、「とりあえず何でも `[]` に入れてAPIを叩く」という実装がある。これ、実務だと「StrictModeによる二重フェッチ」や「競合状態(Race Condition)」の温床になる。

特にNext.jsをはじめとするSSR/RSC全盛の時代であっても、クライアントサイドで完結させなきゃいけないダッシュボードの一部や、インタラクティブなウィジェットでは、`useEffect` によるデータ取得は現役バリバリのテクニックだ。

百聞は一見にしかず。現場でそのまま使える、堅牢なAPI初期取得のコードを見てみよう。

実用的なサンプルコード

import React, { useState, useEffect } from ‘react’;

// 型定義は実務の基本
interface UserProfile {
id: string;
name: string;
email: string;
}

export const UserDashboard: React.FC<{ userId: string }> = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
// 1. 非同期処理を安全に行うためのフラグ(競合・メモリリーク防止)
let isMounted = true;

// 2. AbortControllerでコンポーネントアンマウント時の通信キャンセルを仕込む
const abortController = new AbortController();

const fetchUserData = async () => {
try {
setLoading(true);
setError(null);

const response = await fetch(`https://api.example.com/users/${userId}`, {
signal: abortController.signal,
});

if (!response.ok) {
throw new Error(‘ユーザー情報の取得に失敗しました。’);
}

const data: UserProfile = await response.json();

// コンポーネントがまだマウントされている場合のみstateを更新
if (isMounted) {
setUser(data);
}
} catch (err: unknown) {
// AbortErrorは意図的なキャンセルなのでエラー扱いしない
if (err instanceof Error && err.name !== ‘AbortError’) {
if (isMounted) {
setError(err.message);
}
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};

fetchUserData();

// 3. クリーンアップ関数で後始末を徹底する
return () => {
isMounted = false; // アンマウント後のsetStateをブロック
abortController.abort(); // 走っている通信を即座にキャンセル
};

}, []); // ⚠️ ここがポイント:userIdの変更を無視して「マウント時」だけに限定する場合の記述
// ※ もしuserIdが変わるたびに再取得したいなら、依存配列に [userId] を入れるべき。
// 今回のテーマに沿って「マウント時一度だけ」を厳密に守るためにあえて空にしている。

if (loading) return

読み込み中…

;
if (error) return

エラー: {error}

;
if (!user) return

ユーザーが見つかりません。

;

return (

);
};

—

3. シニアからの実践アドバイス:空配列を使うときの「3つの鉄則」

現場でこの `[]` パターンを使うとき、以下の3つを心に刻んでおいてほしい。コードレビューで容赦なく突っ込まれるポイントだ。

① 「本当に `[]` でいいのか?」と自問自答する

エフェクト内で使っている外部の変数や関数(propsやstate)が、もしレンダーごとに変わるものだったらどうなる?
Linter(ESLintの `react-hooks/exhaustive-deps`)が警告を出してくれたらラッキーだが、無理やり `// eslint-disable-next-line` で黙らせるのは「バグを飼い慣らす」のと同じだ。その変数は本当に固定値か?それとも `useCallback` でメモ化すべきものか?立ち止まって考えよう。

② クリーンアップ関数(Cleanup Function)をサボるな

「一度だけ実行するんだから後始末なんていらないでしょ」というのは素人の考え方だ。
特に開発環境の `React.StrictMode` では、コンポーネントの「マウント ➔ アンマウント ➔ 再マウント」が意図的に高速でシミュレートされる。空配列であっても、タイマーのセットやWebSocketの接続、今回の例のような非同期通信のキャンセル処理を行わないと、メモリリークや「存在しないコンポーネントへのsetState」というReact特有の赤いエラー画面を踏むことになる。

③ ライブラリの選定を視野に入れる

もし「APIの初期取得とキャッシュ、ローディング・エラー管理」を毎回自前で `useEffect` に書いているなら、それは車輪の再発明かもしれない。実務では `TanStack Query (React Query)` や `SWR` を使うのがデファクトスタンダードだ。これらを使えば、面倒な `useEffect` のボイラープレートから解放され、より宣言的で堅牢なコードが書けるようになる。

—

おわりに

依存配列の `[]` は、諸刃の剣だ。
仕組みを正しく理解し、ブラウザの挙動やライフサイクルに思いを馳せながら使えば、これほどシンプルで強力な味方はいない。

「なんとなく動くからよし」ではなく、「なぜここで動くのか、何がクリーンアップされるべきなのか」を説明できるエンジニアになろう。君の書くコードが、次のチームのプロダクトを救う基盤になる。さあ、エディタに戻ってコードを磨き上げようぜ!

コメント

タイトルとURLをコピーしました