やあ。Reactの沼へようこそ。
現場でコードを書いていて、「なぜかメモリリークが起きる」「APIリクエストが重複する」「謎のタイミングで副作用が走る」…そんな壁にぶち当たっていないか?
多くのエンジニアが `useEffect` を「とりあえず動く魔法の箱」だと思っているが、実はこれ、Reactの心臓部を直接いじる外科手術のようなものなんだ。特に、誰もが曖昧にしがちな「クリーンアップ関数の実行タイミング」を正確に把握しているかどうかで、君の書くコードの「格」が劇的に変わる。
今日は、Reactが裏側でどう動いているのか、そしてプロとしてどう副作用を飼いならすべきか、その深淵を覗いていこう。
—
1. クリーンアップ関数は「いつ」呼ばれるのか?
まず、大前提を叩き込んでおこう。`useEffect` の中で返す関数(クリーンアップ関数)は、魔法のように適当なタイミングで動くわけじゃない。Reactのレンダリングパイプラインにおいて、「次の副作用を実行する直前」か「コンポーネントがアンマウントされる直前」に同期的に処理される。
ここが重要だ。ブラウザの描画パイプラインとの兼ね合いで言うと、`useEffect` はブラウザのペイントが終了した後、非同期的に実行される。つまり、クリーンアップ関数が走るタイミングは、ユーザーの目に見える画面の更新よりも後回しになることが多い。
なぜこれが重要なのか?
例えば、タイマーの解除やWebSocketの切断を怠ると、Reactが新しいUIを描画している最中に、古い副作用が裏で暴れ回ることになる。これが「メモリリーク」や「意図しないstateの更新」を引き起こす元凶だ。
—
2. 実践的コード:クリーンアップの挙動を可視化する
理屈だけじゃ腹に落ちないよな。実際にコンソールを叩いて、クリーンアップがどういう順序で走るか確認できるコードを書いた。これをエディタに貼り付けて、挙動を目に焼き付けてみてくれ。
import { useState, useEffect } from ‘react’;
const CleanupDemo = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`[副作用開始] count: ${count}`);
// クリーンアップ関数
return () => {
console.log(`[クリーンアップ] 前回のcount: ${count}`);
};
}, [count]); // countが変わるたびに実行される
return (
Count: {count}
);
};
export default CleanupDemo;
実行結果から読み解く真実
ボタンを何度か押してみると、コンソールにはこう出るはずだ。
1. `[副作用開始] count: 0`
2. (ボタン押下)
3. `[クリーンアップ] 前回のcount: 0`
4. `[副作用開始] count: 1`
ここから分かる重要な事実:「新しい副作用が走る前に、必ず前のクリーンアップが完了する」。この順序が保証されているからこそ、僕らは安心してリソースの後片付けを任せられるんだ。
—
3. 実務で必ず陥る「落とし穴」と対策
現場では、単にタイマーを消すだけじゃない。APIリクエストのキャンセルなどは典型的な課題だ。特にReact 18以降の `Strict Mode` では、開発中にわざとマウント・アンマウントを繰り返すから、クリーンアップが正しく書けていないと地獄を見る。
現場で使える「AbortController」パターン
fetch APIを使う際、クリーンアップをサボると、コンポーネントが消えた後にレスポンスが返ってきて `setState` を叩き、警告が出る。「コンポーネントがアンマウントされた後に更新しようとした」というアイツだ。
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(‘/api/data’, { signal: controller.signal });
const data = await response.json();
setData(data);
} catch (err) {
if (err.name === ‘AbortError’) {
console.log(‘リクエストはキャンセルされました’);
}
}
};
fetchData();
// クリーンアップでリクエストを即座に破棄する
return () => controller.abort();
}, []);
このように、「副作用を仕込むときには、必ず掃除の仕方もセットで書く」。これがシニアへの第一歩だ。
—
最後に:君へのアドバイス
「`useEffect` を減らすこと」が最強の最適化だと言う人もいるが、僕はそうは思わない。むしろ、「副作用のライフサイクルを正確に制御できること」こそが、中級から上級へステップアップするための最大の武器になる。
クリーンアップ関数は、ただの「後片付け」じゃない。「過去の自分(古い副作用)と決別し、新しい状態を受け入れるための儀式」なんだ。
次に `useEffect` を書くとき、その中にある「リソース」がいつ生まれて、いつ死ぬべきなのかを、もう一度だけ立ち止まって考えてみてほしい。そうすれば、君が書くReactアプリは、もっと堅牢で、もっと軽やかなものになるはずだ。
何か詰まったら、またいつでも聞いてくれ。応援しているよ。

コメント