`exhaustive-deps` は「Linterの気まぐれ」ではない:Reactのリアクティブ・システムを真に支配する
Reactを書き始めて数年経つと、誰もが一度は `eslint-plugin-react-hooks` が発する警告に苛立ちを覚えるはずだ。「この変数は依存配列に入れたくないんだ、無限ループするから」「関数を `useCallback` で囲むのが面倒だ」。
しかし、ここで警告を無視して `// eslint-disable-line react-hooks/exhaustive-deps` を魔法の呪文のように唱え始めた瞬間、君のアプリケーションは「時限爆弾」を抱えることになる。今日は、なぜこのルールが単なるコードスタイルではなく、Reactの計算モデルそのものを守るための「最後の砦」なのかを、アーキテクチャの深層から紐解いていく。
—
1. 依存配列の「省略」が招く、Reactの同期不全
Reactの `useEffect` を単なる「副作用を実行するフック」だと思っているなら、それは大きな誤解だ。本質的には、「レンダリング結果と同期すべき外部の状態を、宣言的に定義する仕組み」である。
依存配列を省略したり、意図的に欠落させたりするということは、Reactの「宣言的な同期プロセス」に対して、「君が見ている世界は嘘だ」と教え込む行為に等しい。
発生する典型的なバグ:Stale Closure(古いクロージャ)
例えば、チャットアプリで最新のユーザー名を取得してログを出す処理を考えてみよう。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 依存配列に userId を入れ忘れると、コンポーネントが再レンダリングされても
// このクロージャは「最初の一回目」の userId を握りしめたまま離さない。
fetch(`/api/user/${userId}`).then(res => res.json()).then(setUser);
}, []); // 警告を無視して空配列にすると、userIdが変わっても古いリクエストが走り続ける
return
;
}
このコードの何が恐ろしいか? ユーザーのクリック操作で `userId` が切り替わっても、エフェクト内では永遠に「最初のユーザー」が参照され続ける。非同期処理の競合が発生し、UIには別のユーザーの情報が表示されるという、デバッグが極めて困難な「不整合」が静かに進行する。
—
2. 「関数を依存配列に入れたくない」という甘え
多くのエンジニアが `useCallback` を避ける理由として、「コードが冗長になる」ことを挙げる。しかし、これはReactのレンダリングパイプラインを理解していない証拠だ。
コンポーネント内で定義された関数は、再レンダリングのたびに「別の参照先を持つ新しいインスタンス」として生成される。これを依存配列に入れれば、当然エフェクトは毎回再実行される。だが、それは「Reactが正しく動いている証拠」であって、バグではない。
もし再実行を防ぎたいのであれば、それは「関数を再生成しない(useCallback)」か、「関数をコンポーネントの外に出す(純粋関数化)」というアーキテクチャ上の設計判断を下すべきタイミングなのだ。
—
3. 禁断の「例外」を扱うための高度な戦略
もちろん、すべてのケースで全ての依存関係を配列に含められるわけではない。しかし、抜け道を探す前に、まずは以下の「リファクタリングの正攻法」を検討してほしい。
A. レンダリングに不要な値は `useRef` に逃がす
「値の変化は検知したいが、変化のたびにエフェクトを再起動したくない」というケースがある。その場合は `ref` を活用する。
const savedCallback = useRef(callback);
// 常に最新のコールバックを保持しておくが、依存配列には含めない
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const id = setInterval(() => savedCallback.current(), 1000);
return () => clearInterval(id);
}, []); // 依存関係を空にしても、ref経由で最新の関数を呼び出せる
B. 関数自体を副作用の「内側」で定義する
もしその関数がそのエフェクトでしか使われないなら、依存配列を汚染させないために、エフェクト関数の内部に移動させるべきだ。
useEffect(() => {
// エフェクト内で完結させることで、依存配列から関数を排除できる
const fetchData = async () => {
const data = await api.fetch(userId);
// …
};
fetchData();
}, [userId]); // 依存関係が明確になり、バグの温床が消滅する
—
結論:アーキテクトとしての矜持
`eslint-plugin-react-hooks` の警告を「消す」作業は、単なるLinterを通すための儀式ではない。それは、君が書いたコードの「ライフサイクル」が、Reactのエンジンと正しく握手をしているかを確認する「品質保証のプロセス」だ。
依存配列を省略したくなったら、自分自身にこう問いかけてほしい。
「私は今、Reactのリアクティブ・システムをハックして、未来のバグと引き換えに数分の実装時間を買おうとしていないか?」
堅牢なアプリケーションとは、魔法のようなトリックで作られるものではない。ルールを正しく理解し、泥臭い再計算や状態管理を愚直に積み重ねた先にしか存在しないのだ。
今日からその `// eslint-disable-line` を消そう。そして、必要であれば `useCallback` や `useRef` を駆使して、Reactの流儀に従ってアプリケーションを再構築してほしい。君のコードは、それだけで劇的に安定するはずだ。

コメント