概要
エラーが発生した際に fallback UI と「再試行」ボタンを表示し、ボタン押下で key を変えて子コンポーネントを再マウントするリトライ付き ErrorBoundary。ネットワーク一時障害など回復可能なエラーに有効なパターン。
インストール
# 追加インストールは不要
実装
再試行付き ErrorBoundary
// components/RetryErrorBoundary.tsx
import { Component, type ReactNode } from "react";
type Props = {
children: ReactNode;
fallback?: (props: { error: Error; retry: () => void }) => ReactNode;
};
type State = {
hasError: boolean;
error: Error | null;
retryKey: number;
};
export class RetryErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, retryKey: 0 };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
// retryKey を増やすと子コンポーネントが再マウントされる
retry = () => {
this.setState((prev) => ({
hasError: false,
error: null,
retryKey: prev.retryKey + 1,
}));
};
render() {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback({
error: this.state.error,
retry: this.retry,
});
}
// デフォルト fallback
return (
<div className="rounded border border-red-200 bg-red-50 p-6">
<p className="mb-2 text-sm font-medium text-red-700">
エラーが発生しました
</p>
<p className="mb-4 font-mono text-xs text-red-500">
{this.state.error.message}
</p>
<button
onClick={this.retry}
className="rounded bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700"
>
再試行
</button>
</div>
);
}
// retryKey を key に渡すことで変化時に子を再マウント
return (
<div key={this.state.retryKey}>{this.props.children}</div>
);
}
}
エラーを起こすコンポーネント(デモ用)
// components/UnstableComponent.tsx
"use client";
import { useState } from "react";
export function UnstableComponent() {
const [shouldThrow, setShouldThrow] = useState(false);
if (shouldThrow) {
throw new Error("ネットワークエラー: データの取得に失敗しました");
}
return (
<div className="rounded border p-6">
<p className="mb-4 text-sm text-gray-700">コンポーネントが正常に表示されています</p>
<button
onClick={() => setShouldThrow(true)}
className="rounded bg-gray-800 px-4 py-2 text-sm text-white hover:bg-gray-700"
>
エラーを発生させる
</button>
</div>
);
}
デフォルト fallback を使う例
// app/page.tsx
"use client";
import { RetryErrorBoundary } from "@/components/RetryErrorBoundary";
import { UnstableComponent } from "@/components/UnstableComponent";
export default function Page() {
return (
<main className="mx-auto max-w-lg p-8">
<h1 className="mb-6 text-2xl font-bold">再試行 ErrorBoundary デモ</h1>
<RetryErrorBoundary>
<UnstableComponent />
</RetryErrorBoundary>
</main>
);
}
カスタム fallback を使う例
// app/custom/page.tsx
"use client";
import { RetryErrorBoundary } from "@/components/RetryErrorBoundary";
import { UnstableComponent } from "@/components/UnstableComponent";
export default function Page() {
return (
<main className="mx-auto max-w-lg p-8">
<RetryErrorBoundary
fallback={({ error, retry }) => (
<div className="rounded-xl bg-yellow-50 p-6 text-center">
<p className="mb-1 text-base font-bold text-yellow-800">
データを読み込めませんでした
</p>
<p className="mb-4 text-xs text-yellow-600">{error.message}</p>
<button
onClick={retry}
className="rounded-full bg-yellow-500 px-6 py-2 text-sm font-medium text-white hover:bg-yellow-600"
>
もう一度試す
</button>
</div>
)}
>
<UnstableComponent />
</RetryErrorBoundary>
</main>
);
}
ポイント
retryKeyを state に持ち、再試行ボタンで+1することで子コンポーネントが再マウントされる。keyが変わると React は古いツリーを破棄して新しくマウントするため、エラー状態がリセットされるfallbackを render prop(関数)として受け取ることで、呼び出し元がerrorとretryを受け取りカスタム UI を自由に定義できるreact-suspense-boundaryとの違い:react-suspense-boundaryはReact.lazyのロード失敗を扱う。このパターンはレンダリング中やイベントハンドラ以外でスローされた一般的なエラーを捕捉してリトライ導線を提供する- ErrorBoundary はイベントハンドラ内のエラー(
onClickなど)を捕捉しない。イベントハンドラ内のエラーはtry/catchで処理する componentDidCatchにconsole.errorや外部エラー監視サービス(Sentry など)のレポートを追加すると本番運用に向く