第4回
useEffectのクリーンアップ:メモリリークを防ぐ正しい実装
useEffectのクリーンアップ関数を解説します。タイマー・イベントリスナー・非同期処理のキャンセルなど、アンマウント時に必要なクリーンアップの実装方法をメモリリーク防止の観点から学べます。
·13分で読める
たける
カウントダウンタイマーを実装したんですが、別のページに移動してもカウントが続いてて、コンソールに「Can't perform a React state update on an unmounted component」ってエラーが出てます。
りこ
クリーンアップを書いていないから。setIntervalを開始したなら、コンポーネントが消えるときにclearIntervalする。useEffectは「副作用を終わらせる方法」も一緒に書く。
クリーンアップとは
useEffect で開始した処理(タイマー・イベントリスナー・WebSocket接続など)は、コンポーネントがアンマウントされたあとも動き続けることがあります。これがメモリリークの原因です。
クリーンアップ関数を返すことで、Reactはコンポーネントのアンマウント時(または次の副作用実行前)にそれを呼び出します。
useEffect(() => {
// 副作用を開始
const subscription = someAPI.subscribe();
// クリーンアップ関数を返す
return () => {
subscription.unsubscribe(); // アンマウント時に実行
};
}, []);タイマーのクリーンアップ
setInterval や setTimeout は使い終わったら必ず解除します。
function Countdown({ seconds }: { seconds: number }) {
const [remaining, setRemaining] = useState(seconds);
useEffect(() => {
if (remaining <= 0) return;
const timerId = setInterval(() => {
setRemaining((prev) => prev - 1);
}, 1000);
// クリーンアップ:アンマウント時にタイマーを止める
return () => clearInterval(timerId);
}, [remaining]);
if (remaining <= 0) return <p>時間切れ!</p>;
return <p>残り {remaining} 秒</p>;
}function DelayedMessage({ delay }: { delay: number }) {
const [message, setMessage] = useState('');
useEffect(() => {
const timerId = setTimeout(() => {
setMessage('表示されました!');
}, delay);
return () => clearTimeout(timerId); // アンマウント時にキャンセル
}, [delay]);
return <p>{message}</p>;
}イベントリスナーのクリーンアップ
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll); // クリーンアップ
};
}, []);
return <div className="scroll-indicator">{scrollY}px</div>;
}function KeyboardShortcut({ onSave }: { onSave: () => void }) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
onSave();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onSave]);
return null;
}非同期処理のキャンセル
コンポーネントがアンマウントされた後でstateを更新しようとすると、Reactが警告を出します(React 18以降は警告のみですが、設計上は防ぐべきです)。
フラグを使う方法
function UserCard({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let isMounted = true; // マウント中かどうかのフラグ
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (isMounted) { // マウント中のみ更新
setUser(data);
}
});
return () => {
isMounted = false; // アンマウント時にフラグをfalseに
};
}, [userId]);
return user ? <div>{user.name}</div> : <div>読み込み中...</div>;
}AbortControllerを使う方法(推奨)
より現代的な方法は AbortController でfetchリクエスト自体をキャンセルします。
function ArticleViewer({ articleId }: { articleId: number }) {
const [article, setArticle] = useState<Article | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const controller = new AbortController(); // キャンセル用コントローラー
async function fetchArticle() {
try {
setIsLoading(true);
const response = await fetch(`/api/articles/${articleId}`, {
signal: controller.signal, // AbortSignalを渡す
});
const data = await response.json();
setArticle(data);
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return; // キャンセルによるエラーは無視
}
console.error(err);
} finally {
setIsLoading(false);
}
}
fetchArticle();
return () => {
controller.abort(); // クリーンアップ:リクエストをキャンセル
};
}, [articleId]);
if (isLoading) return <div>読み込み中...</div>;
return article ? <article>{article.title}</article> : null;
}WebSocket接続のクリーンアップ
function LiveChat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/rooms/${roomId}`);
ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
return () => {
ws.close(); // アンマウント時に接続を閉じる
};
}, [roomId]);
return (
<ul>
{messages.map((msg, i) => <li key={i}>{msg}</li>)}
</ul>
);
}クリーンアップが実行されるタイミング
1. コンポーネントがマウントされる
→ useEffectのコールバックが実行される
2. 依存配列の値が変わる(再実行前)
→ 前回のクリーンアップが実行される
→ 新しいコールバックが実行される
3. コンポーネントがアンマウントされる
→ クリーンアップが実行される
たける
AbortControllerってfetch専用ですか? APIを叩いてる途中でページを離れたとき、そのレスポンスを捨てたいケースが多くて。
りこ
fetchのキャンセルに使う。コンポーネントがアンマウントされた後にstateを更新しようとするとReactがエラーを出す。AbortControllerでfetchをキャンセルすることでそれを防ぐ。
翔太
インフラ的に補足すると、クライアントがリクエストを切断してもサーバー側の処理は続くことがある。AbortControllerはブラウザ・ネットワーク間のキャンセルで、サーバーまで止めるには別途設計が必要。
まとめ
useEffectから関数を返すとクリーンアップとして実行されるsetInterval/setTimeoutはclearInterval/clearTimeoutで解除するaddEventListenerはremoveEventListenerで解除する- fetch のキャンセルには
AbortControllerを使う - クリーンアップを怠るとメモリリークや予期しない状態更新の原因になる
次の第5回では、コンポーネントツリー全体で値を共有する useContext を学びます。