React State Update on Unmounted Component エラーの原因と解決方法
Reactを使用していると、コンソールに「Can't perform a React state update on an unmounted component」というエラーメッセージが表示されることがあります。このエラーは開発時に頻繁に遭遇するもので、多くの初心者開発者を悩ませています。しかし、このエラーの原因と解決方法を理解すれば、安全で効率的なコンポーネント開発ができるようになります。
本記事では、このエラーが発生する理由から、具体的な解決手順、実装例まで、わかりやすく解説していきます。
第1章:エラーの原因を理解する
そもそも「unmounted component」とは何か
Reactコンポーネントにはライフサイクルがあります。コンポーネントが生成され、DOMに挿入される「マウント」という過程と、DOMから削除される「アンマウント」という過程があります。
「unmounted component」とは、既にDOMから削除されたコンポーネントのことです。つまり、もう画面に表示されていないコンポーネントのことを指します。
なぜこのエラーが発生するのか
このエラーが発生するメカニズムは以下の通りです:
- 非同期処理(APIリクエストなど)を開始する
- その処理が完了する前にコンポーネントがアンマウント(削除)される
- 非同期処理が完了し、setState()でstate更新を試みる
- 既に削除されたコンポーネントのstateを更新しようとするため、エラーが発生
具体的な例を挙げると、ユーザーが画面遷移してコンポーネントが削除されたのに、その直後にAPI通信の応答が返ってきて、stateを更新しようとするというシナリオです。
このエラーが問題である理由
このエラーは単なる警告ではなく、以下の問題を示唆しています:
- メモリリーク:不要な処理がメモリに残り続ける可能性がある
- パフォーマンス低下:蓄積されたメモリリークはアプリケーション全体を遅くする
- 予期しない動作:削除されたコンポーネントへの参照が残る可能性がある
したがって、このエラーを解決することはコード品質を大きく向上させることにつながります。
第2章:解決手順の詳細解説
ステップ1:useEffectのクリーンアップ関数を使用する
Reactの関数コンポーネントでは、useEffectフックを使用して副作用を管理します。重要なのは、useEffectが「クリーンアップ関数」をサポートしているということです。
クリーンアップ関数とは、コンポーネントがアンマウント(または再レンダリング)される際に実行される関数のことです。このクリーンアップ関数の中で、未完了の非同期処理をキャンセルすることで、エラーを防ぐことができます。
ステップ2:AbortControllerの導入
JavaScript標準のAbortControllerAPIを使用すると、リクエストをキャンセルできます。これは、API呼び出しを途中で停止する機能です。
ステップ3:コンポーネント削除時の処理
アンマウント時に確認フラグを設定し、非同期処理完了後の状態更新をガードすることで、エラーを防止できます。
第3章:実装コード例
解決方法1:AbortControllerを使った実装(推奨)
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// AbortControllerを作成
const controller = new AbortController();
const signal = controller.signal;
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(
`https://api.example.com/users/${userId}`,
{ signal }
);
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
const data = await response.json();
// signalがキャンセルされていないかチェック
if (!signal.aborted) {
setUser(data);
}
} catch (err) {
// AbortErrorの場合は何もしない
if (err.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
return;
}
if (!signal.aborted) {
setError(err.message);
}
} finally {
if (!signal.aborted) {
setLoading(false);
}
}
};
fetchUser();
// クリーンアップ関数:コンポーネントアンマウント時にリクエストをキャンセル
return () => {
controller.abort();
};
}, [userId]);
if (loading) return 読み込み中...;
if (error) return エラー: {error};
return (
{user?.name}
メール: {user?.email}
);
}
export default UserProfile;
解決方法2:フラグを使った実装
import React, { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// マウント状態を追跡するフラグ
let isMounted = true;
const fetchProducts = async () => {
try {
const response = await fetch('https://api.example.com/products');
const data = await response.json();
// コンポーネントがまだマウント状態のときのみ更新
if (isMounted) {
setProducts(data);
setLoading(false);
}
} catch (error) {
if (isMounted) {
console.error('エラーが発生しました:', error);
setLoading(false);
}
}
};
fetchProducts();
// クリーンアップ関数
return () => {
isMounted = false;
};
}, []);
if (loading) return 読み込み中...;
return (
{products.map((product) => (
{product.name}
価格: ${product.price}
))}
);
}
export default ProductList;
解決方法3:カスタムフックを使った実装
import { useEffect, useRef, useCallback } from 'react';
// 再利用可能なカスタムフック
function useFetch(url) {
const [data, setData] = useRef(null);
const [loading, setLoading] = useRef(true);
const [error, setError] = useRef(null);
const isMountedRef = useRef(true);
const refetch = useCallback(async () => {
const controller = new AbortController();
try {
setLoading.current = true;
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMountedRef.current) {
setData.current = result;
setError.current = null;
}
} catch (err) {
if (err.name !== 'AbortError' && isMountedRef.current) {
setError.current = err.message;
}
} finally {
if (isMountedRef.current) {
setLoading.current = false;
}
}
return () => controller.abort();
}, [url]);
useEffect(() => {
isMountedRef.current = true;
refetch();
return () => {
isMountedRef.current = false;
};
}, [refetch]);
return {
data: data.current,
loading: loading.current,
error: error.current,
refetch
};
}
// 使用例
function App() {
const { data, loading, error } = useFetch('https://api.example.com/data');
if (loading) return 読み込み中...;
if (error) return エラー: {error};
return {JSON.stringify(data)};
}
export default App;
第4章:よくある間違いと対策
間違い1:クリーンアップ関数を使わない
// ❌ 間違った例
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data)); // クリーンアップなし
}, []);
return {data?.name};
}
この例では、API呼び出しが完了する前にコンポーネントがアンマウントされると、エラーが発生します。
間違い2:依存配列を指定しない
// ❌ 間違った例
function BadComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 依存配列がないため、毎回レンダリングするたびにタイマーが作成される
}); // 依存配列がない!
return {count};
}
間味い3:非同期処理で直接stateを更新
// ❌ 間違った例
useEffect(() => {
async function fetchData() {
const res = await fetch('/api/data');
const data = await res.json();
setData(data); // アンマウント時の保護がない
}
fetchData();
}, []);
// ✅ 正しい例
useEffect(() => {
let isMounted = true;
async function fetchData() {
const res = await fetch('/api/data');
const data = await res.json();
if (isMounted) {
setData(data);
}
}
fetchData();
return () => {
isMounted = false;
};
}, []);
第5章:デバッグ方法
開発者ツールでの確認方法
Reactの開発モードでは、このエラーが発生した際に詳細なスタックトレースが表示されます。
1. ブラウザのコンソールを開く(F12キー)
2. 警告メッセージをクリックしてスタックトレースを確認する
3. エラーが発生しているコンポーネントと行番号を特定する
ログを活用したデバッグ
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
console.log('フェッチ開始');
const response = await fetch('/api/data');
const data = await response.json();
console.log('isMounted:', isMounted);
if (isMounted) {
setData(data);
console.log('state更新完了');
} else {
console.log('コンポーネントがアンマウント済みなため、更新をスキップ');
}
};
fetchData();
return () => {
console.log('クリーンアップ関数実行');
isMounted = false;
};
}, []);
第6章:ベストプラクティス
外部ライブラリの活用
大規模なプロジェクトでは、useQueryやuseSWRなどのデータフェッチライブラリの使用を検討しましょう。これらのライブラリはメモリリーク対策が組み込まれています。
// useQueryを使った例
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
});
if (isLoading) return 読み込み中...;
if (error) return エラー: {error.message};
return {data.name};
}
TypeScriptでの型安全な実装
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }): JSX.Element {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted: boolean = true;
const fetchUser = async (): Promise => {
try {
const response = await fetch(`/api/users/${userId}`);
const data: User = await response.json();
if (isMounted) {
setUser(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err.message : '未知のエラー');
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchUser();
return () => {
isMounted = false;
};
}, [userId]);
if (loading) return 読み込み中...;
if (error) return エラー: {error};
if (!user) return ユーザーが見つかりません;
return (
{user.name}
{user.email}
);
}
export default UserProfile;
まとめ
「React state update on unmounted component」エラーは、適切な理解と対策により完全に解決できます。本記事の要点をまとめます:
重要なポイント
- 原因:非同期処理完了後に削除済みコンポーネントのstateを更新しようとすることが原因
- 解決方法1:AbortControllerを使ってリクエストをキャンセルする(推奨)
- 解決方法2:isMountedフラグを使ってstateの条件付き更新を行う
- 解決方法3:useQueryなどのライブラリを使う
実装時の注意点
- useEffectのクリーンアップ関数を必ず実装する
- 依存配列を正しく指定する
- 非同期処理後の状態更新は必ずガードする
- TypeScriptを使う場合は型安全性を確保する
このエラーはReactを使う限り何度も遭遇するものですが、本記事の内容を理解することで、安全で堅牢なコンポーネント開発ができるようになります。特にAbortControllerを使った方法は最も標準的で推奨される方法であり、プロダクション環境での使用に最適です。
ぜひこれらの方法を実装に取り入れ、エラーのない高品質なReactアプリケーション開発を心がけてください。

