お疲れ。最近、君が書いたコンポーネントのコードレビューをしたんだけどさ……正直、「あ、これまたやってるな」って箇所がいくつかあったんだよね。
特に、`useEffect` の依存配列(dependency array)まわり。
「とりあえず値が変わったっぽかったから、プリミティブ値だし突っ込んでおいた」みたいな実装が見受けられた。動くには動くけどさ、それ、本当に意図した通りのライフサイクルで動いてる? 無駄な再レンダリングやエフェクトの暴発を引き起こしてないかい?
今日は、Reactの心臓部である「プリミティブ値による依存配列の制御」について、ブラウザの裏側の動きも含めて徹底的に叩き込む。中級から一段上のシニアへステップアップするための必須知識だから、しっかりついてきてほしい。
—
1. 依存配列の判定基準:Reactは裏側で何をしているのか?
まず大前提として、`useEffect` や `useMemo`、`useCallback` の依存配列に指定した値が「変わったかどうか」を、Reactはどうやって判定していると思う?
「そりゃ、比較してるんでしょ」――その通り。だけど、その比較アルゴリズムの正体を知っている人は意外と少ない。
Reactは、依存配列の比較に `Object.is()` というJavaScriptの標準仕様を使っている。
`Object.is()` とは何か?
ざっくり言うと、`===`(厳密等価演算子)の厳格版だ。基本的には `===` と同じ挙動をするんだけど、主に以下の2点で `===` と異なる。
1. `Object.is(NaN, NaN)` が `true` になる(`===` だと `false` になる)
2. `Object.is(+0, -0)` が `false` になる(`===` だと `true` になる)
まあ、Reactのコンテキストで重要なのは 1 の `NaN` の話よりも、「数値(number)、文字列(string)、真偽値(boolean)といったプリミティブ値は、値そのものが同じであれば、メモリ上のどこにあろうとも同一とみなされる」 という点だ。
プリミティブ値なら安心して依存配列に突っ込んでいいのか?
「じゃあ、プリミティブ値なら何度書き換えても安全だし、依存配列にポンポン入れていいんですね?」
……結論から言うと、半分正解で、半分は大間違いだ。
プリミティブ値自体の比較は高速だし、値が変化した時(`Object.is()` が `false` を返す時)にだけエフェクトが走る仕組みは非常に美しくできている。
問題なのは、「そのプリミティブ値が、本当に『エフェクトを再実行すべきトリガー』としてふさわしいか?」 という設計の文脈のほうなんだよ。
—
2. 現場でやりがちな「プリミティブ値のアンチパターン」
よくある現場の失敗例を見せよう。
例えば、親から渡されたIDやフラグをもとに、データフェッチを行うコンポーネントがあったとする。
// 【アンチパターン】よくある現場のコード
import React, { useState, useEffect } from ‘react’;
type UserProfileProps = {
userId: string;
debugMode: boolean; // 開発用のフラグなど
};
export const UserProfile: React.FC
const [userData, setUserData] = useState(null);
// userIdが変わったらフェッチしたい、という意図
useEffect(() => {
let isMounted = true;
async function fetchUserData() {
console.log(`Fetching for user: ${userId}, Debug: ${debugMode}`);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (isMounted) {
setUserData(data);
}
}
fetchUserData();
return () => {
isMounted = false;
};
}, [userId, debugMode]); // ← ここ!debugModeを依存配列に入れてしまっている
return
;
};
さて、このコードの何が問題かわかるかい?
`debugMode` は確かにプリミティブ値(boolean)だし、`Object.is()` で安全に比較できる。
しかし、親コンポーネントの再レンダリングの拍子に、この `debugMode` の参照(あるいは親の都合での再描画)が絡んだり、あるいは「本来は `userId` が変わった時だけにフェッチしたいのに、`debugMode` がトグルされただけで不要なAPIリクエストが走る」というバグを生む温床になるんだ。
プリミティブ値だからといって、コンポーネント内のあらゆる状態やPropsを思考停止で依存配列に並べるのは、シニアとしては絶対に避けたいところだね。
—
3. 実践!堅牢なプリミティブ値の依存配列コントロール
じゃあ、実務でどう書くのが正解なのか。
クリーンアップ関数や依存配列の制御を完璧に盛り込んだ、現場でそのまま使える実用的なサンプルコードを用意した。じっくり見てほしい。
import React, { useState, useEffect } from ‘react’;
type ArticleViewerProps = {
articleId: number; // 記事のID(プリミティブ値:number)
autoSaveInterval: number; // 自動保存の間隔(秒)(プリミティブ値:number)
};
export const ArticleViewer: React.FC
articleId,
autoSaveInterval,
}) => {
const [content, setContent] = useState
const [statusMessage, setStatusMessage] = useState
// 1. 記事IDの変化に追従するデータフェッチの副作用
useEffect(() => {
// 競合状態(Race Condition)を防ぐためのフラグ
let isCurrent = true;
const loadArticle = async () => {
setStatusMessage(‘読み込み中…’);
try {
// 実際のAPIコールを想定
const res = await fetch(`/api/articles/${articleId}`);
const data = await res.json();
// 非同期処理が完了した時点で、このエフェクトが破棄されていなければ状態を更新
if (isCurrent) {
setContent(data.content);
setStatusMessage(‘読み込み完了’);
}
} catch (error) {
if (isCurrent) {
setStatusMessage(‘読み込みに失敗しました’);
}
}
};
loadArticle();
// クリーンアップ関数:
// articleIdが変わる、またはアンマウントされた時に前の非同期処理の結果を無視する
return () => {
isCurrent = false;
};
}, [articleId]); // プリミティブ値である articleId のみが変化した時だけ再実行される
// 2. タイマーなどの継続的な副作用の制御
useEffect(() => {
// autoSaveInterval が 0 以下の場合はタイマーを起動しない安全策
if (autoSaveInterval <= 0) return;
const timerId = setInterval(() => {
console.log(`[AutoSave] 記事ID: ${articleId} の下書きを保存中… (間隔: ${autoSaveInterval}秒)`);
// ここに自動保存のロジックが入る
}, autoSaveInterval 1000);
// クリーンアップ関数:
// インターバル中に autoSaveInterval や articleId が変わったら、古いタイマーを必ずクリアする
return () => {
clearInterval(timerId);
};
}, [articleId, autoSaveInterval]); // 関連するプリミティブ値を正確に指定
return (
記事ビュアー (ID: {articleId})
ステータス: {statusMessage}
);
};
このコードのシニア的解説ポイント
1. プリミティブ値の適切なスコープ分け
`articleId` と `autoSaveInterval` はどちらもプリミティブ(数値)だけど、「データの取得」と「定期実行タイマー」という異なる関心事ごとに `useEffect` を綺麗に分離している。これにより、依存配列の組み合わせがシンプルになり、予測不可能なバグを防げる。
2. クリーンアップ関数による安全性の担保
非同期処理(`loadArticle`)では `isCurrent` フラグを使い、タイマー処理では `clearInterval` を確実に実行している。依存配列のプリミティブ値が切り替わった瞬間、古いエフェクトの残骸がメモリリークや意図しない状態書き込みを起こさないための鉄則だ。
3. `Object.is` の特性を活かした高速な比較
`articleId` や `autoSaveInterval` はプリミティブなので、Reactは内部の `Object.is` でコストをかけることなく一瞬で変更を検知できる。余計な `useMemo` や `useCallback` でラップする必要すらなく、素のままで十分に高速に動作する。
—
4. シニアからのまとめ
フロントエンド開発において、`useEffect` は諸刃の剣だ。
扱いを誤るとコンポーネントをスパゲッティにするし、正しく使えば極めて堅牢でパフォーマンスの高いUIを実現できる。
プリミティブ値の依存配列はその中でも最も基本であり、だからこそ「なぜこの値を配列に入れているのか」「本当にこの値が変わった時だけに限定すべきか」という設計思想がモロに出る部分だ。
今日話した `Object.is()` の動き、そして関心事の分離とクリーンアップの徹底。これらを意識するだけで、君が書くコードの品質はワンランクもツーランクも上がるはずだ。
次のコードレビューでは、もっと洗練された依存配列を見せてくれよ。期待してるぜ!

コメント