Reactの「Strict Mode」が副作用の二重実行? 驚かないで! クリーンアップ関数の大切さを学ぼう
Web制作や開発の世界に足を踏み入れたばかりの皆さん、そしてReactに触れて間もない皆さん、こんにちは!
今回は、React開発で時々「あれ?なんかおかしいぞ?」と感じるかもしれない、ちょっと不思議な挙動についてお話しします。それは、開発環境で有効になる「Strict Mode(厳格モード)」という機能が、私たちの書いた「副作用」をわざと二回実行してしまう、というもの。
「え、なんでわざわざ二回も? バグなの?」なんて、不安になっちゃいますよね。大丈夫、これはバグじゃないんです。むしろ、私たちのコードをより頑丈にするための、Reactからの「愛のあるお仕置き」みたいなものなんです(笑)。
今回は、このStrict Modeの二重実行がどういう仕組みなのか、そして、それを乗り越えるために絶対に欠かせない「クリーンアップ関数」の書き方と、その確認方法を、身近な例え話を交えながら、優しく丁寧に解説していきますね。
副作用って、なんだっけ? お掃除とセットで考えてみよう!
まず、「副作用」って言葉が出てくると、ちょっと身構えちゃうかもしれませんが、Reactの世界では、コンポーネントがレンダリングされるたびに「何か追加で起こること」を指します。
例えば、
- APIからデータを取ってくる
- タイマーをセットする
- ブラウザのイベントリスナーを登録する
こういった、画面に直接表示されるもの以外の「裏側で動く処理」が、代表的な副作用です。
Reactでは、この副作用を扱うために `useEffect` というフックを使いますよね。
ここで、ちょっと想像してみてください。
あなたが新しいお部屋に引っ越してきて、ピカピカの家具を配置したとします。
- `useEffect` の実行: 新しい家具を部屋に置く作業
- コンポーネントのレンダリング: 家具が配置されて、部屋が完成する様子
さて、ここで問題です。
もし、この部屋で「換気のために窓を開ける」という作業があったらどうでしょう?
窓を開けて、新鮮な空気を取り込む。これは、部屋を快適にするための、いわば「副作用」のようなものです。
でも、もしこの「窓を開ける」作業を、間違って二回やってしまったら?
一度開けた窓を、また開けようとする…? ちょっと変ですよね。
もしかしたら、風が強すぎてカーテンがバタバタしたり、余計なものが飛んでくるかもしれません。
Strict Modeの「二重実行」は、まさにお掃除の練習!
ReactのStrict Modeは、まさにこの「二重実行」を開発環境で意図的に行うことで、私たちに「お掃除、ちゃんとできてる?」と問いかけてくるんです。
Strict Modeが有効な場合、 `useEffect` の中身は、開発環境では「一度実行」された後、まるで「リハーサル」のように、もう一度「実行」されます。
これは、コンポーネントがマウント(画面に表示される)された時と、アンマウント(画面から消える)される直前、というタイミングで、 `useEffect` の「クリーンアップ関数」が正しく動くかどうかをチェックするためなんです。
クリーンアップ関数って、何者?
先ほどの部屋の例えで言うと、窓を開ける作業で、もし「二度開け」を防ぎたいなら、どうすればいいでしょう?
「一度開けたら、閉める」というルールを決めておく、とか。
あるいは、窓を開けたら、その作業を「元に戻す」ための後処理を考えておく、というのもありそうです。
Reactの `useEffect` では、この「後処理」の役割を担うのが「クリーンアップ関数」なんです。
`useEffect` から `return` で関数を返すことで、このクリーンアップ関数を定義できます。
useEffect(() => {
// ここに、コンポーネントがマウントされた時に実行したい処理を書きます。
// 例えば、タイマーをセットしたり、イベントリスナーを登録したり。
console.log(‘コンポーネントがマウントされました!’);
// ★ここがクリーンアップ関数です!★
return () => {
// コンポーネントがアンマウントされる直前、
// または、useEffectが再実行される直前に実行されます。
// ここで、useEffectの開始時に行った処理を元に戻します。
console.log(‘クリーンアップ実行! 前回の処理を元に戻します。’);
// 例:タイマーのクリア、イベントリスナーの解除など
};
}, []); // 依存配列が空なので、一度だけ実行されるはず…ですが、Strict Modeでは…?
このクリーンアップ関数は、
- コンポーネントが画面から消えるとき(アンマウント)
- そして、Strict Modeでは、 `useEffect` が再実行される直前
に実行されるように設計されています。
つまり、 Strict Modeが副作用を二重実行するのは、このクリーンアップ関数が、再実行の前に「前の処理をちゃんと綺麗にしてくれるか」を確認するためなんです。
買い物で例えると、もっと分かりやすい?
ちょっと別の例え話をしましょう。
あなたは、お目当ての服を買いに、デパートにやってきました。
1. お店に入る(マウント): まず、お店に入ります。
2. 服を見る(useEffectの実行): いろいろな服を手に取って、試着したりします。
3. 店員さんに声をかける(副作用の処理): 「この服、試着してもいいですか?」と店員さんに声をかけます。
4. 試着室へ(useEffectのreturn部分): 店員さんが試着室に案内してくれます。
さて、ここでStrict Modeが介入します。
Strict Modeは、あなたが「試着室へ行く」という行動を、一度だけでなく、二度試してみるんです。
- 一度目の「試着室へ」: あなたは試着室に入り、服を試着します。
- Strict Modeの「もう一回!」: Strict Modeは、「あれ?もしかしたら、試着室に入る前に『〇〇さん、試着室をご案内します』って店員さんが言った声、ちゃんと聞こえてた?」「試着室に入った後、ドアをしっかり閉め忘れてない?」って心配するんです。
- クリーンアップ関数の出番!: そこで、Strict Modeは、あなたが次の試着(次のuseEffect実行)に進む前に、一度「試着室から出る」という行動を促します。これがクリーンアップ関数!「試着が終わったら、服を畳んで元に戻す」「試着室のドアを閉める」といった、前の状態を綺麗にする作業をします。
もし、あなたが「試着が終わったら、服を畳む」というクリーンアップをちゃんとやっていれば、Strict Modeが二回実行しても、問題なく次のステップに進めます。
でも、もし「試着が終わったのに、服をぐしゃぐしゃのままにして、試着室を出てしまったら…」?
次に同じ服を試着しようとした時に、すでにぐしゃぐしゃだったり、試着室が散らかっていたりして、嫌な気持ちになりますよね。
Strict Modeは、そんな「後始末の甘さ」を、開発段階で早期に発見させてくれる、ありがたい存在なんです。
依存配列の「空っぽ `[]`」と「何もない」の違い
ここで、 `useEffect` の「依存配列」について、少しだけ触れておきましょう。
`useEffect` の第二引数に渡す配列ですね。
useEffect(() => {
// …
}, [stateA, stateB]); // stateAかstateBが変わったら、useEffectが再実行される
- 依存配列に何か指定がある場合: `[stateA, stateB]` のように、特定のstateやpropsを指定すると、それらが変更された時に `useEffect` が再実行されます。この時、Strict Modeは、再実行の前にクリーンアップ関数を呼び出します。
- 依存配列が空っぽ `[]` の場合: `[]` を指定すると、コンポーネントがマウントされた時(一度だけ)に実行されるはずでした。しかし、Strict Modeでは、この「一度だけ」の実行を、開発環境で「二回」実行します(クリーンアップもセットで)。これは、コンポーネントがマウント・アンマウントされた時の挙動を、より厳密にテストするためです。
- 依存配列を省略した場合: `useEffect(() => { … });` のように、依存配列を省略すると、コンポーネントがレンダリングされるたびに `useEffect` が実行されます。これは、意図しない無限ループを引き起こす可能性があるので、通常は推奨されません。
Strict Modeの二重実行は、特に依存配列が `[]` の場合に顕著に現れるので、「あれ? `[]` なのに2回実行されるぞ?」と戸惑うことが多いかもしれません。でも、これは正常な動作なのです。
クリーンアップ関数は、どうやって書くのが正解?
では、Strict Modeの二重実行にも耐えられる、しっかりとしたクリーンアップ関数をどう書けばいいのか、具体的なコードで見てみましょう。
例1:タイマーのクリーンアップ
import React, { useState, useEffect } from ‘react’;
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 1. コンポーネントがマウントされたら、1秒ごとにcountを増やすタイマーをセット
console.log(‘TimerComponent: useEffectが実行されました。タイマーをセットします。’);
const timerId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 2. クリーンアップ関数:コンポーネントがアンマウントされる時、
// またはStrict Modeで再実行される直前に、タイマーをクリアする
return () => {
console.log(‘TimerComponent: クリーンアップ実行! タイマーをクリアします。’);
clearInterval(timerId); // ★タイマーをクリア!★
};
}, []); // 依存配列は空なので、マウント時に一度だけ実行されるはず…ですが、Strict Modeでは二回実行されます。
console.log(‘TimerComponent: レンダリング’);
return (
タイマー
カウント: {count}
);
}
export default TimerComponent;
このコードのポイント:
- `useEffect` の中で `setInterval` を使ってタイマーをセットしています。
- `return` で定義されたクリーンアップ関数の中で、 `clearInterval(timerId)` を呼び出しています。
- これにより、コンポーネントが画面から消える時や、Strict Modeで再実行される前に、前のタイマーがちゃんと停止されることが保証されます。
Strict Modeでの実行イメージ:
1. `TimerComponent` がマウントされる。
2. `useEffect` が実行され、タイマーがセットされる。(コンソールに「タイマーをセットします。」)
3. `TimerComponent` がレンダリングされる。
4. Strict Modeが「リハーサル」として、もう一度 `useEffect` のクリーンアップを呼び出す。
5. クリーンアップ関数が実行され、タイマーがクリアされる。(コンソールに「タイマーをクリアします。」)
6. Strict Modeが「リハーサル」として、もう一度 `useEffect` の本体を実行する。
7. `useEffect` が再度実行され、新しいタイマーがセットされる。(コンソールに「タイマーをセットします。」)
8. `TimerComponent` がレンダリングされる。
このように、Strict Modeでは、マウント時とアンマウント(または再実行直前)のセットで、クリーンアップ関数が2回実行されるのが確認できます。
しかし、クリーンアップ関数が正しく実装されていれば、タイマーが二重に動いてしまったり、予期せぬバグが発生したりすることはありません。
例2:イベントリスナーのクリーンアップ
import React, { useState, useEffect } from ‘react’;
function EventListenerComponent() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// 1. コンポーネントがマウントされたら、マウスの移動イベントを監視
console.log(‘EventListenerComponent: useEffectが実行されました。イベントリスナーを登録します。’);
const handleMouseMove = (event) => {
setMousePosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener(‘mousemove’, handleMouseMove); // ★イベントリスナーを登録★
// 2. クリーンアップ関数:コンポーネントがアンマウントされる時、
// またはStrict Modeで再実行される直前に、イベントリスナーを解除する
return () => {
console.log(‘EventListenerComponent: クリーンアップ実行! イベントリスナーを解除します。’);
window.removeEventListener(‘mousemove’, handleMouseMove); // ★イベントリスナーを解除!★
};
}, []); // 依存配列は空。Strict Modeでは二回実行される。
console.log(‘EventListenerComponent: レンダリング’);
return (
マウス座標
X: {mousePosition.x}, Y: {mousePosition.y}
);
}
export default EventListenerComponent;
このコードのポイント:
- `useEffect` の中で `window.addEventListener` を使って、マウスの移動イベントを拾うようにしています。
- クリーンアップ関数では、 `window.removeEventListener` を使って、登録したイベントリスナーを解除しています。
- これにより、コンポーネントが不要になった時に、メモリリーク(不要なイベントリスナーが残り続けること)を防ぎます。
Strict Modeでの実行イメージ:
1. `EventListenerComponent` がマウントされる。
2. `useEffect` が実行され、イベントリスナーが登録される。(コンソールに「イベントリスナーを登録します。」)
3. `EventListenerComponent` がレンダリングされる。
4. Strict Modeが「リハーサル」として、クリーンアップ関数を呼び出す。
5. クリーンアップ関数が実行され、イベントリスナーが解除される。(コンソールに「イベントリスナーを解除します。」)
6. Strict Modeが「リハーサル」として、もう一度 `useEffect` の本体を実行する。
7. `useEffect` が再度実行され、新しいイベントリスナーが登録される。(コンソールに「イベントリスナーを登録します。」)
8. `EventListenerComponent` がレンダリングされる。
こちらも、クリーンアップ関数が正しく実装されていれば、イベントリスナーが二重に登録されてしまう心配はありません。
開発環境で「二重実行」を体験してみよう!
これらのコードを、ご自身のReactプロジェクトで試してみてください。
`src/App.js` などで、これらのコンポーネントを呼び出してみると、開発サーバーを起動した時に、コンソールにログが流れるのが確認できるはずです。
// src/App.js の例
import React from ‘react’;
import TimerComponent from ‘./TimerComponent’; // 上記のTimerComponent.jsをインポート
// import EventListenerComponent from ‘./EventListenerComponent’; // 上記のEventListenerComponent.jsをインポート
function App() {
return (
{/
);
}
export default App;
開発サーバーを再起動したり、ページをリロードしたりするたびに、 `useEffect` の実行とクリーンアップのログが交互に表示されるのを見て、「おお、これがStrict Modeの二重実行か!」と実感できるはずです。
まとめ:Strict Modeは、あなたのコードを強くする!
Strict Modeによる副作用の二重実行は、最初は戸惑うかもしれませんが、これはReactが「あなたのコードを、より安全で、より予測可能なものにしてくださいね」というメッセージなんです。
- Strict Modeは、開発環境でのみ動作します。 本番環境では、この二重実行は行われません。
- クリーンアップ関数は、副作用の「後始末」です。 タイマーのクリア、イベントリスナーの解除、不要になったDOM要素の削除など、useEffectで始めた処理を元に戻す役割があります。
- クリーンアップ関数を正しく実装することで、Strict Modeの二重実行にも問題なく対応できます。 メモリリークや予期せぬバグを防ぐためにも、クリーンアップ関数の実装は非常に重要です。
もし、Strict Modeで二重実行されて困ったな、と感じたら、まずは `useEffect` の中に `return () => { … }` という形でクリーンアップ関数が実装されているか、そしてその中で「始めた処理をちゃんと元に戻す」処理が書かれているか、を確認してみてください。
最初は少し難しく感じるかもしれませんが、このクリーンアップ関数の考え方をマスターすれば、Reactでの副作用の扱いは格段に上手になります。
皆さんのReact開発が、より楽しく、よりスムーズになることを願っています!
もしつまずくことがあっても、大丈夫。一歩ずつ、ゆっくり進んでいきましょうね!

コメント