「画面遷移を繰り返すと、アプリが重くなる…」「memory leak warningが消えない…」
こんな悩み、私たちも何度も経験しました。
でも、Reactのライフサイクル理論を深掘りする前に、まずは「正しいクリーンアップの書き方」を知り、目の前の警告をサクッと解消したい――そんな現場目線で、実務に効くパターン集をまとめました。
用語解説:React
Facebookが開発した、UI構築のためのJavaScriptライブラリ。コンポーネント単位でWebアプリを効率的に開発できる。用語解説:useEffect
Reactの関数コンポーネントで副作用(データ取得やイベント登録など)を扱うためのフック。依存配列で実行タイミングを制御できる。用語解説:ライフサイクル
コンポーネントが生成・更新・破棄される一連の流れ。Reactではマウント・更新・アンマウントの各タイミングで処理を挟める。用語解説:クリーンアップ関数
useEffect内でreturnすることで、コンポーネントのアンマウント時や副作用の再実行前に実行される後片付け用の関数。用語解説:メモリリーク
本来解放されるべきメモリが不要な参照や解除漏れで残り続け、アプリの動作が重くなる現象。
(Reactの基本やuseStateとの違いについては『【保存版】ReactのuseStateとuseEffectの違いとは?初心者が実務で迷わない使い分け完全ガイド』をご参照ください)
1. まずは比較!タイマーが止まらない失敗例と成功例
「なぜかタイマーが止まらない…」
その原因、クリーンアップ関数の書き忘れかもしれません。
// ❌ タイマーが止まらない失敗例
import React, { useEffect, useState } from 'react';
function BadTimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
}, []); // クリーンアップがないため、タイマーが残り続ける
return (
<div>
<h2>❌ Bad Timer</h2>
<p>Count: {count}</p>
</div>
);
}
// ✅ クリーンアップでタイマーを止める成功例
import React, { useEffect, useState } from 'react';
function GoodTimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => {
clearInterval(intervalId); // ✅ アンマウント時にタイマーを停止
};
}, []);
return (
<div>
<h2>✅ Good Timer</h2>
<p>Count: {count}</p>
</div>
);
}
まずはこの違いを押さえましょう。
2. React useEffectメモリリークの「なぜ?」とリスク
メモリリーク――
それは、イベントリスナーやタイマー、非同期処理の解除忘れが主な原因です。
- イベントリスナーの解除漏れ
- タイマーの停止忘れ
- 非同期処理のキャンセル漏れ
これらが残ると、
不要なメモリ消費や「アンマウント後の状態更新」警告につながります。
放置すると、アプリの動作が重くなり、バグの温床にも。
私たちがよくハマる“落とし穴”です。
用語解説:イベントリスナー
ユーザーの操作(クリックやスクロールなど)を検知し、処理を実行するための仕組み。addEventListenerで登録し、removeEventListenerで解除する。用語解説:タイマー(setTimeout/setInterval)
一定時間後や一定間隔で処理を実行するJavaScriptの仕組み。clearTimeout/clearIntervalで解除できる。用語解説:非同期処理
API通信やタイマーなど、処理の完了を待たずに次の処理を進めるプログラミング手法。Promiseやasync/awaitで記述する。
3. コピペOK!useEffectクリーンアップ関数の正しい書き方パターン集
(1)イベントリスナーのクリーンアップ
// 基本パターン
import React, { useEffect } from 'react';
function MyComponent() {
const handleScroll = () => console.log('Scrolled!');
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>スクロールしてください</div>;
}
// カスタムフック化
function useEventListener(eventType, handler, element = window) {
useEffect(() => {
element.addEventListener(eventType, handler);
return () => element.removeEventListener(eventType, handler);
}, [eventType, handler, element]);
}
(2)タイマー(setTimeout, setInterval)のクリーンアップ
// setTimeout
import React, { useEffect, useState } from 'react';
function TimeoutComponent() {
const [message, setMessage] = useState('...');
useEffect(() => {
const timerId = setTimeout(() => setMessage('2秒後に表示!'), 2000);
return () => clearTimeout(timerId);
}, []);
return <div>{message}</div>;
}
// setInterval
function IntervalWithStateComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <p>Count: {count}</p>;
}
(JavaScriptのタイマーや非同期処理の基礎については『非同期処理とは?JavaScriptとJavaの実例でわかる使い方・落とし穴・解決法』も参考になります)
(3)非同期処理のキャンセルとAbortController
import React, { useEffect, useState } from 'react';
function DataFetcherComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(json => {
if (!signal.aborted) setData(json);
})
.catch(e => {
if (e.name !== 'AbortError') console.error(e);
});
return () => abortController.abort();
}, []);
return <div>Data: {JSON.stringify(data)}</div>;
}
用語解説:AbortController
fetchなどの非同期通信を途中でキャンセルできるブラウザAPI。signalを渡してリクエストを管理し、abort()で中断できる。
(4)アンマウント後のsetState警告を回避
import React, { useEffect, useState, useRef } from 'react';
function SafeAsyncUpdateComponent() {
const [data, setData] = useState(null);
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
const fetchData = async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
if (isMounted.current) {
setData('データ取得完了!');
}
};
fetchData();
return () => {
isMounted.current = false;
};
}, []);
return <div>{data || 'データ取得中...'}</div>;
}
ぜひコードをコピペして、まずは動かしてみてください。
4. メモリリークの発見と診断:Chrome DevTools活用術
兆候を見逃さない
・「Can’t perform a React state update on an unmounted component…」警告
・アプリの動作が徐々に重くなる
Chrome DevToolsでの手順
- DevToolsを開き「Memory」タブへ
- 「Heap snapshot」を2回取得し、比較
- Δ Sizeが増え続ける項目(Detached DOM treeなど)を確認
- 保持元(Retainers)をたどり、リーク元を特定
この流れで、どこで解除漏れが起きているかを見つけられます。
5. useEffectを正しく使うためのベストプラクティス
(1)依存配列の最適化
- 含めるべきもの:副作用に影響するprops, state, 関数
- 含めない方がよいもの:毎回新しくなるオブジェクトや関数(useCallbackやuseMemoでメモ化推奨)
- 空配列[]の意図:マウント時のみ実行
(2)ESLintの活用
eslint-plugin-react-hooksのexhaustive-depsルールで、依存配列の記述漏れを自動検出!
用語解説:ESLint
JavaScript/TypeScriptのコード品質やバグを自動検出する静的解析ツール。ルールを追加してプロジェクト全体の品質を保てる。用語解説:依存配列(Dependency Array)
useEffectの第2引数。ここに指定した値が変化したときだけ副作用が再実行される。
// .eslintrc.json
{
"extends": [
"react-app",
"plugin:react-hooks/recommended"
]
}
(3)カスタムフックでDRYなコードへ
// useTimeout カスタムフック例
import { useEffect, useRef } from 'react';
function useTimeout(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(id);
}
}, [delay]);
}
(4)TypeScriptで型安全なクリーンアップ
型定義を活用することで、誤った引数やプロパティアクセスを事前に防げます。
堅牢性アップ&バグ減少に直結します。
用語解説:TypeScript
JavaScriptに型付け機能を加えた言語。型安全によりバグを減らし、大規模開発でも安心して使える。用語解説:型安全
変数や関数の型を明示することで、誤った使い方をコンパイル時に検出できる仕組み。
(TypeScriptの導入や型定義の実践例は『TypeScriptの始め方|Node.jsとVSCodeで学ぶ開発環境構築ガイド【初心者向け完全解説】』や『TypeScript Promise型定義完全解説|any撲滅と使い分け』もご参照ください)
(5)useEffect vs useLayoutEffect
- useEffect:DOM描画後に非同期で実行(基本はこちら)
- useLayoutEffect:DOM描画前に同期実行(レイアウト調整など限定的に)
重い処理はuseEffectで。
パフォーマンスを意識して使い分けましょう。
6. よくある質問(FAQ)
-
Q1. クリーンアップ関数は毎回書くべき?
A. イベントリスナーやタイマーなど、ライフサイクル外で続く副作用には必須です。 -
Q2. 依存配列にオブジェクトや関数を含めると?
A. 無限ループや意図しない再実行の原因に。useCallbackやuseMemoでメモ化を。 -
Q3. 外部ライブラリ利用時もクリーンアップは必要?
A. はい。destroy()などの破棄メソッドを必ず呼びましょう。 -
Q4. useEffectの代わりにuseMemoやuseCallbackで副作用は?
A. できません。副作用にはuseEffectを使いましょう。 -
Q5. Strict Modeの警告は本番でも発生する?
A. はい。警告は本番でも起こり得る問題です。必ず修正を。 -
Q6. メモリリーク対策でどれくらいパフォーマンス向上?
A. SPAの画面遷移や長時間利用時の重さが劇的に改善します。 -
Q7. Class Componentでも同じ対策が必要?
A. はい。componentWillUnmountでクリーンアップを。
まとめ
React useEffectのメモリリークは、私たち“モダン挑戦者”がよく直面する課題です。
でも、正しいクリーンアップパターンを知れば、「警告ゼロ」「パフォーマンス改善」も夢じゃありません。
- イベントリスナーやタイマーの解除
- 非同期処理のキャンセル
- Chrome DevToolsやESLintの活用
- カスタムフックやTypeScriptでの堅牢化
これらを組み合わせて、
DRYで堅牢なReactアプリ開発を一緒に目指しましょう。
ぜひ手元のプロジェクトで、今日から実践してみてください!