更新時にだけ処理を実行する方法(依存配列の活用)
生徒
「ある変数が変わったときだけ処理を実行したいです。毎回走ると困るんですがどうすればいいですか?」
先生
「ReactではuseEffectの第二引数、依存配列を使います。依存配列に監視したい状態やプロパティを書けば、その値が変更されたときだけ副作用が走ります。」
生徒
「なぜ毎回走ってしまうことがあるんですか?」
先生
「依存配列を空にするか書かないと挙動が変わります。書き方と合わせて、オブジェクトや関数などの扱い方も理解しましょう。」
1. 依存配列とは何か?基本の仕組み
依存配列(dependency array)とは、useEffectの第二引数に渡す配列です。配列の中に指定した値(stateやprops)が変化したときだけ、useEffect内の処理が実行されます。これにより不要な再実行を防ぎ、パフォーマンスを向上できます。
2. 代表的な使い方 三パターン
- 書かない場合:レンダリングごとに実行されます。毎回走るので注意。
- 空配列 []:マウント時に一度だけ実行します(componentDidMount相当)。
- [value]:valueが変わるときだけ実行します(更新時の制御)。
3. 実例:IDが変わったときだけデータを取得する
ユーザーIDが変わったときだけAPIを呼ぶ例です。依存配列にidを入れることで、idが変わった瞬間だけfetchが走ります。
import React, { useState, useEffect } from "react";
function UserProfile({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (!id) return;
const ac = new AbortController();
fetch(`/api/users/${id}`, { signal: ac.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {});
return () => ac.abort();
}, [id]);
return <div>{user ? user.name : "読み込み中"}</div>;
}
export default UserProfile;
4. 注意点:関数やオブジェクトを依存にする場合
関数や配列、オブジェクトは参照が変わると毎回違うと判定されるため、依存配列に入れると頻繁に再実行されることがあります。この問題はuseCallbackやuseMemoでメモ化して解決します。
5. 例:関数を依存にする場合の対処
import React, { useState, useEffect, useCallback } from "react";
function Search() {
const [query, setQuery] = useState("");
const fetchResults = useCallback(async () => {
const r = await fetch(`/api/search?q=${query}`);
return r.json();
}, [query]); // queryが変わったときだけ再生成
useEffect(() => {
fetchResults();
}, [fetchResults]); // fetchResultsが変わったときだけ実行
}
6. ESLintのexhaustive-depsルール
Reactの公式ルールであるexhaustive-depsは、依存配列に必要な値を自動で指摘してくれます。警告に従うとバグを防げますが、意図的に省く場合はコメントで理由を残すことが大切です。
7. ステールクロージャに注意
useEffect内で古い状態を参照してしまう「ステールクロージャ」が起こることがあります。最新の値を使いたい場合は依存配列に値を入れるか、refを使って最新値を参照する設計にします。
8. クリーンアップとキャンセル
非同期処理やサブスクリプションを行う際は、クリーンアップを必ず実装します。AbortControllerやreturnでのクリーンアップ関数を使い、アンマウントや依存変更時に安全に処理を止めます。
9. 実践的なパターンとパフォーマンス
依存配列を正しく使うことでAPI呼び出しの回数を減らし、不要な再描画を防げます。複雑な依存関係は小さなuseEffectに分割して責務を分けると読みやすくなります。またデバウンスやスロットリングを併用するとユーザー操作に優しい設計になります。
10. よくあるトラブルと対応
- 依存配列を書き忘れて毎回実行される → 必要な値を明示する
- オブジェクトを直接依存にしてしまう → useMemoで安定化
- 警告を無視してバグを生む → ESLintの指摘に従うか理由をコメント
最後に
依存配列はReactのライフサイクルを細かく制御する強力な仕組みです。正しく使えばパフォーマンスと信頼性が上がります。まずは小さな例から試して、依存関係がどのように動くか体験してみてください。
まとめ
React開発において避けては通れない「副作用の制御」ですが、その中心にあるのがuseEffectの依存配列です。本記事では、特定の状態やプロパティが変化したときだけ処理を実行するための基礎から、実戦で役立つ応用テクニックまでを詳しく見てきました。
フロントエンドエンジニアとしてステップアップするためには、単に「動くコード」を書くだけでなく、「無駄のない効率的なコード」を書くことが求められます。依存配列の仕組みを理解することは、パフォーマンスの最適化だけでなく、予期せぬ無限ループや古いデータ(ステールクロージャ)によるバグを防ぐための第一歩です。
依存配列の使い分けを再確認
改めて、依存配列の3つのパターンを整理しておきましょう。これらを使い分けるだけで、コンポーネントの挙動を意図通りにコントロールできるようになります。
| 依存配列の指定 | 実行タイミング | 主な用途 |
|---|---|---|
| 指定なし(第2引数自体を省略) | レンダリングのたびに毎回実行 | デバッグ用のログ出力など(基本は非推奨) |
空の配列 [] |
マウント時の1回のみ実行 | APIの初回データ取得、イベントリスナーの登録 |
値あり [value] |
指定した値が変化した時のみ実行 | 検索キーワードに応じた再検索、ID変更時の詳細取得 |
実践的な実装例:検索フィルターの最適化
学んだ内容を活かして、実際のアプリケーションでよく見かける「カテゴリー選択とキーワード検索」を組み合わせたコードを書いてみましょう。依存配列に複数の値を指定することで、どちらかが変わったときだけデータを再取得する仕組みを構築します。
import React, { useState, useEffect } from "react";
function ProductList() {
const [products, setProducts] = useState([]);
const [category, setCategory] = useState("all");
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true;
const fetchProducts = async () => {
setLoading(true);
try {
// カテゴリーや検索ワードをクエリパラメータとして送信
const response = await fetch(`/api/products?cat=${category}&q=${search}`);
const data = await response.json();
if (isMounted) {
setProducts(data);
setLoading(false);
}
} catch (error) {
console.error("データの取得に失敗しました", error);
if (isMounted) setLoading(false);
}
};
fetchProducts();
// クリーンアップ関数で競合状態を防ぐ
return () => {
isMounted = false;
};
}, [category, search]); // カテゴリーか検索ワードが変わったときだけ動く
return (
<div className="p-4">
<h3 className="mb-3">商品一覧</h3>
<div className="d-flex gap-2 mb-4">
<select
className="form-select"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="all">すべて</option>
<option value="electronics">家電</option>
<option value="clothing">衣類</option>
</select>
<input
type="text"
className="form-control"
placeholder="キーワードを入力..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{loading ? (
<p>読み込み中...</p>
) : (
<ul className="list-group">
{products.map(product => (
<li key={product.id} className="list-group-item">
{product.name}
</li>
))}
</ul>
)}
</div>
);
}
export default ProductList;
さらに高度な制御のために
実際の現場では、入力のたびにAPIを叩くとサーバーに負荷がかかるため、**デバウンス(Debounce)**という手法を組み合わせるのが一般的です。これは、ユーザーが文字入力を止めてから一定時間後に処理を実行する仕組みです。これもuseEffectと依存配列、そしてクリーンアップ関数を組み合わせることで美しく実装できます。
また、JavaScriptの「参照型」という性質を忘れてはいけません。コンポーネント内で定義した関数やオブジェクトを依存配列に入れると、中身が変わっていなくてもレンダリングのたびに「別物」と判定されてしまいます。これを防ぐためにuseCallbackやuseMemoを使って、関数の実体をメモリに保持(メモ化)しておく技術もセットで覚えておきましょう。
ReactのHooksは非常に強力ですが、正しく制御しないと思わぬ挙動を招きます。常に「この副作用はいつ実行されるべきか?」を自問自答しながらコードを書く癖をつけるのが、上達への近道です。最初はESLintの警告に助けてもらいながら、徐々に感覚を掴んでいきましょう。
生徒
先生、まとめのお話を聞いて、依存配列の重要性がさらによく分かりました!「空の配列」と「値を入れた配列」で、こんなに役割が変わるんですね。
先生
その通り。特に空の配列 [] は、JavaScriptで言うところの「ページを開いた時の一回きりの処理」を表現するのに欠かせないんだ。でも、実際のアプリだと「この値が変わった時だけやり直したい」というケースがほとんどだよね。
生徒
さっきのコード例にあった「検索ワード」の更新とかですね。でも、依存配列にたくさんの変数を入れすぎると、管理が大変になりそうな気がします……。
先生
いい視点だね!もし依存配列がパンパンになってきたら、それはその useEffect が「色々なことをやりすぎている」というサインかもしれない。そんな時は、処理の内容ごとに useEffect を分割することを検討してごらん。コードがぐっと読みやすくなるよ。
生徒
なるほど、分割すればそれぞれの役割がはっきりしますね。あと、関数を依存配列に入れるときに useCallback を使うという話も驚きました。関数も「値」として変化をチェックされているんですね。
先生
そうなんだ。JavaScriptでは関数もオブジェクトの一種だから、レンダリングのたびに「新しい関数」が作られてしまうんだよ。だから、中身が同じでも React は「おっ、新しい関数になったから副作用を実行しなきゃ!」と勘違いしてしまう。それを防ぐのがメモ化の役割だね。
生徒
奥が深い……!でも、クリーンアップ関数の isMounted を使ったキャンセル処理とか、プロっぽい書き方が知れてワクワクしてきました。これからはパフォーマンスも意識して書いてみます!
先生
その意気だよ。まずは console.log を使って、いつ、どのタイミングで処理が走っているかを目で確認しながら練習してみてね。理屈で覚えるより、動かして覚えるのが一番の近道だからね。