エラーの境界とデバッグ
トムはEコマースのバックエンドを担当していたが、本番サイトで「ホワイトスクリーン」の問題が発生した。ユーザーからは、注文詳細ページを開くと画面が完全に真っ白になるという報告があり、コンソールにはエラー
Cannot read properties of undefined (reading 'name')が表示されていた。原因は、レンダリングフェーズ中に子コンポーネントによってスローされた、キャッチされていないJavaScriptエラーであり、これがReactコンポーネントツリー全体のクラッシュを引き起こしていました。トムは、個々のコンポーネントの障害がページ全体の使いやすさに影響を与えないよう、アプリケーション内にエラーバウンダリーシステムを構築する必要があります。
1. 学習内容
- エラー境界の原則とベストプラクティス
- React DevTools プロファイラーでフレームチャートを分析する方法
- Profiler コンポーネントは、コンポーネントのレンダリング時間を測定します
- よくあるパフォーマンス問題の診断と解決策
2. 概念図
以下の図は、エラー境界の捕捉プロセスとプロファイラの分析チェーンを示しています:
flowchart TD
A[React Component tree] --> B[Error Boundary]
B --> C{Is there an error in the rendering of the child component??}
C -->|No| D[Normal Rendering]
C -->|is | E[getDerivedStateFromError]
E --> F[Update state.hasError = true]
F --> G[Rendering fallback UI]
G --> H{Click to retry??}
H -->|is | I[Reset state]
I --> A
H -->|No| J[Stay in the lower division UI]
K[React DevTools Profiler] --> L[Recording Component Rendering]
L --> M[Flame Pattern Analysis]
M --> N["Identify Time-Consuming Components(Yellow/Red)"]
N --> O[React.memo / useMemo Optimization]
style B fill:#e3f2fd,stroke:#1565c0
style E fill:#fff3e0,stroke:#e65100
style G fill:#e8f5e9,stroke:#2e7d32
style K fill:#f3e5f5,stroke:#7b1fa2
3. 実際の事例
| エラー処理レベル | 適用範囲 | 復旧戦略 | 適用シナリオ |
|---|---|---|---|
| try/catch | 単一の非同期操作 | リトライ/フォールバック | APIリクエスト、Promise操作 |
| エラー境界 | 子コンポーネントツリーのレンダリングエラー | フォールバックUI + 再試行ボタン | コンポーネントのホワイトスクリーン保護 |
| グローバルな未処理の拒否 | キャッチされなかった Promise エラー | ログ報告 | 包括的な監視 |
| window.onerror | グローバル同期エラー | ログ報告 | 包括的監視 |
| React DevTools | 開発中のデバッグ | トラブルシューティング | パフォーマンス・レンダリングに関する問題のトラブルシューティング |
トムの注文詳細ページは、以下の要素で構成されています:
OrderPage
├── OrderHeader (Order Number、Status)
├── OrderItems (Product List)
│ └── OrderItem × N(Single Item,Includes price calculation)
├── ShippingInfo (Shipping Information)
└── PaymentInfo (Payment Information)
このオンライン上の不具合の原因は、特定の注文の商品データからpriceフィールドが欠落していたため、OrderItemコンポーネントがitem.price.toFixed(2)にアクセスした際にTypeErrorエラーが発生し、その結果、OrderPage全体が空白画面となってしまったことでした。
正しい対処法は、OrderItems エリアをエラーバウンダリーで囲むことです。そうすれば、商品リストのレンダリングに失敗した場合でも、注文ヘッダーと支払い情報は正常に表示されます。さらに、トムは React DevTools プロファイラーを使用してパフォーマンスのボトルネックを特定する方法を学ぶ必要があります。
(1) エラー境界 — コンポーネントレベルのセーフティネット
エラーバウンダリーは、React が提供する 宣言型のエラー処理メカニズム です。サブツリー内のいずれかのコンポーネントが、レンダリングフェーズ、ライフサイクルメソッド、またはコンストラクタ内でエラーをスローした場合、エラーバウンダリーはそのエラーをキャッチし、アプリケーション全体が空白画面になるのを防ぎ、代替の UI を表示することができます。
注: エラーバウンダリーは現在、クラスコンポーネントでのみ実装可能です(Reactでは、今後のリリースでフック版を提供する予定です)。
(1) ▶ サンプル:汎用的な ErrorBoundary コンポーネント
import { Component, ErrorInfo, ReactNode } from 'react'
interface ErrorBoundaryProps {
children: ReactNode
/** Custom Downgrade UI */
fallback?: ReactNode
/** Error Callback(Submit Sentry etc.) */
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
// Static Methods:Update Based on the Error state
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
// Life Cycle:Performing side effects after catching an error(Log Reporting)
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary An error was caught:', error.message)
console.error('Component Stack:', errorInfo.componentStack)
// Reported to the error monitoring service
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
// Can be integrated into actual projects Sentry:
// Sentry.captureException(error, { extra: errorInfo })
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
// Use Custom fallback Or default to a lower level UI
if (this.props.fallback) {
return this.props.fallback
}
return (
<div
role="alert"
style={{
padding: '32px 24px',
margin: 16,
background: '#fff2f0',
border: '1px solid #ffccc7',
borderRadius: 8,
textAlign: 'center',
}}
>
<h2 style={{ color: '#ff4d4f', margin: '0 0 12px' }}>
A component error occurred
</h2>
<p style={{ color: '#666', marginBottom: 8, fontSize: 14 }}>
{this.state.error?.message || 'An unknown error has occurred'}
</p>
<button
onClick={this.handleReset}
style={{
padding: '6px 20px',
background: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 14,
}}
>
Retry
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
実際のプロジェクトにおけるErrorBoundaryの活用
// Layered Wrapping — Each independent area has its own ErrorBoundary
function OrderPage({ orderId }: { orderId: string }) {
return (
<div>
{/* Order Header Information:It will display correctly even if there is an error below. */}
<ErrorBoundary fallback={<p>Failed to load order</p>}>
<OrderHeader orderId={orderId} />
</ErrorBoundary>
{/* Product List:Errors in one area do not affect other areas */}
<ErrorBoundary
onError={(err) => {
// Rendering Error in the List of Submitted Products
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify({ error: err.message, orderId }),
})
}}
>
<OrderItems orderId={orderId} />
</ErrorBoundary>
{/* Payment Information */}
<ErrorBoundary>
<PaymentInfo orderId={orderId} />
</ErrorBoundary>
</div>
)
}
(2) ▶ サンプル:エラー回復機能を備えた UserProfile コンポーネント
実際のシナリオでは、フォールバック UI を表示するだけでは不十分な場合があり、ユーザーが特定のデータを更新する必要があることもあります。以下に、「再試行」機能を備えたエラーバウンダリの使用例を示します。
import { useState } from 'react'
import ErrorBoundary from './ErrorBoundary'
// Simulating Data Retrieval That Results in Errors
function fetchUserData(userId: number) {
return fetch(`/api/users/${userId}`).then(res => {
if (!res.ok) throw new Error('Failed to retrieve user data')
return res.json()
})
}
// Data display components that may have rendering errors
function UserInfo({ userId }: { userId: number }) {
const [user, setUser] = useState<any>(null)
const [loading, setLoading] = useState(true)
useState(() => {
fetchUserData(userId)
.then(setUser)
.finally(() => setLoading(false))
})
if (loading) return <p>Loading......</p>
// If user Data Structure Exception,An error may occur here
return (
<div>
<h3>{user.name}</h3> {/* possibly:Cannot read properties of undefined */}
<p>{user.profile.bio}</p> {/* possibly:Cannot read properties of undefined */}
</div>
)
}
// Outer Container:With retries key Mechanism
function UserProfile({ userId }: { userId: number }) {
const [retryKey, setRetryKey] = useState(0)
return (
<ErrorBoundary
key={retryKey} // Change key It will unmount and remount the subtree
fallback={
<div style={{ padding: 24, textAlign: 'center' }}>
<p>Error loading user information</p>
<button onClick={() => setRetryKey(k => k + 1)}>
Retry Loading
</button>
</div>
}
>
<UserInfo userId={userId} />
</ErrorBoundary>
)
}
重要なヒント: key={retryKey} リトライがトリガーされた際に、ErrorBoundaryにサブツリーのアンマウントと再作成を行わせ、すべての子コンポーネントの状態をリセットするようにします。
(2) エラー境界では捕捉できないエラー
エラー境界は万能薬ではない。以下の4種類のエラーは検出できない。
| エラーの種類 | 原因 | 解決策 |
|---|---|---|
| イベント処理におけるエラー | レンダリングフェーズ中にイベントハンドラが実行されない | イベント処理ロジックを try/catch ブロックで囲む |
| 非同期コードにおけるエラー | setTimeout や Promise のコールバックは、React のレンダリングサイクル内では実行されない | try/catch または Promise.catch を使用する |
| サーバーサイドレンダリング(SSR)におけるエラー | エラー境界はクライアントサイドでのみ有効 | SSR ではレンダリングを try/catch ブロックで囲む |
| エラー・バウンダリー自身のエラー | それ自体ではキャッチできないエラーをスローする | 最外層で別のエラー・バウンダリーで囲む |
非同期コードにおけるイベント処理と適切なエラー処理
function PaymentForm() {
async function handleSubmit() {
try {
const result = await submitPayment()
// Processed successfully
} catch (error) {
// Asynchronous errors are caught here,Error Boundary That's none of my business
console.error('Payment Failed:', error)
// Display Error UI(For example, setting state)
setError(error instanceof Error ? error.message : 'Payment Failed')
}
}
// Errors in the event must also be used try/catch
function handleClick() {
try {
processPayment()
} catch (error) {
setError('Processing Failed,Please try again.')
}
}
}
(3) React DevToolsのプロファイラを使用したパフォーマンス分析
React DevTools の「Profiler」タブは、コンポーネントのレンダリングパフォーマンスを分析するための主要なツールです。このタブでは、各コンポーネントのレンダリング時間を視覚的に示す「フレイムグラフ」が生成されます。
使用方法
1. Open your browser DevTools → Components Tabs
2. Switch to Profiler Sublabel
3. Click the blue record button(Start Recording)
4. Performing actions on the page(Click、Scrolling, etc.)
5. Click the Stop button(End Recording)
6. View the flame diagram
フレイムチャートの読み方
┌────────────────────────────────────────────┐
│ App (0.3ms) │
│ ├── Navbar (0.2ms) │
│ ├── OrderPage (2.1ms) │
│ │ ├── OrderHeader (0.4ms) ── Gray │
│ │ ├── OrderItems (1.5ms) ── Yellow │
│ │ │ └── OrderItem × 20 (each 0.3ms) │
│ │ └── PaymentInfo (0.2ms) ── Gray │
│ └── Footer (0.1ms) │
└────────────────────────────────────────────┘
- グレー:再レンダリングなし(理想的なシナリオ)
- 青:再レンダリングされたが、処理時間は通常通りだった
- 黄色/赤:レンダリングに時間がかかる。注意が必要
(3) ▶ サンプル:プロファイラ・コンポーネントを使用したレンダリング時間の測定
Reactに組み込まれている<Profiler>コンポーネントは、コード内の特定のコンポーネントのレンダリング時間を正確に測定できるため、パフォーマンス指標の自動監視に適しています:
import { Profiler } from 'react'
type ProfilerPhase = 'mount' | 'update' | 'nested-update'
interface ProfileMetrics {
id: string
phase: ProfilerPhase
actualDuration: number // Actual rendering time for this render(milliseconds)
baseDuration: number // Worst-case runtime for a subtree
startTime: number // Render Start Timestamp
commitTime: number // Submit to DOM timestamp
interactions: Set<any> // Related Interaction Tracking
}
// Performance Monitoring Callbacks
function onRenderCallback(
id: string,
phase: ProfilerPhase,
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
) {
// Record to the performance log
if (actualDuration > 16) { // More than 16ms = Frame drop threshold (60fps)
console.warn(
`[Performance Alerts] ${id} in ${phase} Time Taken per Stage ${actualDuration.toFixed(1)}ms,` +
`More than 16ms Frame Budget!`
)
// Reported to the performance monitoring system
// reportPerformance({ id, phase, actualDuration, baseDuration })
}
// Output from the development environment to the console
if (process.env.NODE_ENV === 'development') {
console.table({
'Components': id,
'Phase': phase,
'Actual time taken(ms)': actualDuration.toFixed(1),
'Benchmark Duration(ms)': baseDuration.toFixed(1),
})
}
}
// Big Data List——Potential Performance Bottlenecks
function ProductList({ products }: { products: Product[] }) {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(3, 1fr)' }}>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</Profiler>
)
}
一般的なパフォーマンス最適化の手法
// 1. React.memo — Avoid Unnecessary Re-rendering
const ProductCard = React.memo(function ProductCard({
product,
}: {
product: Product
}) {
return (
<div style={{ border: '1px solid #eee', padding: 16, borderRadius: 8 }}>
<img src={product.image} alt={product.name} width="100%" />
<h4>{product.name}</h4>
<p>${product.price}</p>
</div>
)
})
// 2. useMemo — Cache the results of expensive computations
function OrderSummary({ items }: { items: OrderItem[] }) {
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => {
// Assuming that complex currency conversions were performed here
return sum + convertCurrency(item.price, item.currency)
}, 0)
}, [items])
return <p>Total:${totalPrice.toFixed(2)}</p>
}
// 3. useCallback — Stable function references
function OrderList({ orders, onSelect }: {
orders: Order[]
onSelect: (id: string) => void
}) {
// ✅ use useCallback Keep references consistent
const handleSelect = useCallback((id: string) => {
onSelect(id)
}, [onSelect])
return orders.map(order => (
<OrderRow key={order.id} order={order} onSelect={handleSelect} />
))
}
(4) React DevToolsのコンポーネントパネルを使用したデバッグ
プロファイラーに加え、React DevTools の「コンポーネント」パネルも、日常的なデバッグに役立つ強力なツールです:
| 機能 | 目的 | 操作 |
|---|---|---|
| コンポーネントツリーを閲覧する | コンポーネントの階層を表示する | [DevTools] → [コンポーネント] をクリックする |
| プロパティ/状態をリアルタイムで表示 | コンポーネントの現在の状態を確認 | コンポーネントを選択して右側のパネルを表示 |
| 状態を直接変更する | さまざまな状態での UI をテストする | 状態の値をダブルクリックして直接編集する |
| コンポーネント検索 | コンポーネントのクイック検索 | Ctrl+F コンポーネント名を入力 |
| ソースコードへ | コンポーネントの実装を表示 | <> アイコンをクリック |
// DevTools Components Panel Examples
<OrderPage>
<ErrorBoundary>
<OrderHeader
orderNumber="ORD-2026-0001" ← Props Real-time Display
status="shipped" ← Can be edited directly during testing
/>
</ErrorBoundary>
<ErrorBoundary>
<OrderItems>
<OrderItem product={...} /> ← State Expand to view
<OrderItem product={...} />
</OrderItems>
</ErrorBoundary>
</OrderPage>
(4) ▶ サンプル:API リクエストのエラー処理 — リトライ機能を用いたデータの取得
function useFetchWithRetry(url, maxRetries = 3) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
const [retries, setRetries] = useState(0)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
setData(json)
} catch (err) {
if (retries < maxRetries) {
setRetries(r => r + 1)
setTimeout(fetchData, 1000 * (retries + 1))
} else {
setError(err.message)
}
} finally {
setLoading(false)
}
}, [url, retries, maxRetries])
useEffect(() => { fetchData() }, [url])
return { data, error, loading, retries, refetch: () => { setRetries(0); fetchData() } }
}
function UserList() {
const { data: users, error, loading, retries, refetch } = useFetchWithRetry('/api/users')
if (loading) return <p>Loading... {retries > 0 && `(retry ${retries})`}</p>
if (error) return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p style={{ color: '#ff4d4f' }}>Error: {error}</p>
<button onClick={refetch} style={{ padding: '8px 16px', cursor: 'pointer' }}>Retry</button>
</div>
)
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)
}
(5) ▶ サンプル:グローバルエラー監視 — Sentry との連携
// lib/errorReporting.ts
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN
function initErrorReporting() {
if (typeof window === 'undefined') return
if (!SENTRY_DSN) return
// Sentry.init({ dsn: SENTRY_DSN, ... })
// Simplified Example:Simulation Using Global Event Listeners
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise:', event.reason)
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: new Date().toISOString(),
})
})
window.addEventListener('error', (event) => {
console.error('Global Error:', event.error)
reportError({
type: 'window.error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
timestamp: new Date().toISOString(),
})
})
}
function reportError(payload) {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {})
}
// app/layout.tsx
function RootLayout({ children }) {
useEffect(() => { initErrorReporting() }, [])
return <html><body>{children}</body></html>
}
❓ よくある質問
try/catchを使用)、非同期コード内のエラー(Promise.catchを使用)、サーバーサイドレンダリング中のエラー、およびエラーバウンダリ内部のエラーです。エラー処理を設計する際には、try/catchとエラーバウンダリを組み合わせて、階層的なアプローチを採用する必要があります。React.memo はすべてのコンポーネントを自動的に最適化しますか?useMemo / useCallbackを使用するか、React.memoに2番目の引数(カスタム比較関数React.memo(Comp, (prev, next) => deepEqual(prev, next)))を渡す必要があります。<Profiler> コンポーネントは本番環境のパフォーマンスに影響を与えますか?<Profiler> コンポーネントは本番環境でわずかなパフォーマンスのオーバーヘッドを引き起こします。開発環境でのみ使用するか、環境変数 {process.env.NODE_ENV === 'development' && <Profiler>...} を通じて制御することをお勧めします。本番環境でパフォーマンスの監視が必要な場合は、専用のパフォーマンス監視ライブラリ(web-vitalsなど)や、Sentryのパフォーマンストレース機能の利用を検討してください。getDerivedStateFromError と componentDidCatch という 2 つのライフサイクルメソッドに依存していますが、これらはクラスコンポーネントでのみサポートされています。Reactチームは、将来的にフック版が提供される可能性を示唆していますが、現時点(React 18/19)では、クラスコンポーネントを使用してのみ実装可能です。クラスベースのエラーバウンダリコンポーネントを作成し、それを関数コンポーネントでラップすることで、エラー回復ロジックを処理することができます。📖 まとめ
- Error Boundary は、React の宣言型エラー処理ソリューションであり、1つのクラッシュによってアプリ全体が空白画面になるのを防ぎます。
- エラー境界は、
getDerivedStateFromErrorおよびcomponentDidCatchのライフサイクルメソッドを連携させることで、クラスコンポーネントのみを使用して実装することができます。 - Changing the
keyin the Error Boundary allows you to reset (remount) the subtree. - Error Boundary は、イベントハンドラ、非同期コード、SSR、または自身内部で発生したエラーを捕捉することはできません
- React DevToolsのプロファイラーは、レンダリング時間をフレイムグラフで表示します。黄色や赤色のコンポーネントは、最適化が必要なものです。
- React.memo、useMemo、useCallback は、React のパフォーマンス最適化における「三本柱」です
📝 練習問題
ErrorBoundaryコンポーネントを作成し、OrderPage: WrapOrderHeader,OrderItems, andPaymentInfoin separateErrorBoundaryコンポーネント内で階層的に使用します。手動でレンダリングエラー(誤ったプロパティの渡しかたなど)を発生させ、影響を受けた領域のみにフォールバック UI が表示され、インターフェースのその他の部分は正常にレンダリングされることを確認します。Profilerコンポーネントを使用して、100個のリスト項目を含むコンポーネントのレンダリング時間を測定します。React.memoによる最適化を行った後、再度レンダリング時間を測定し、2回の測定結果の差をactualDurationで比較して、最適化の効果を確認します。- ChromeでReact DevToolsのプロファイラーを開き、ページの操作(検索、フィルタリング、ソートなど)を記録し、フレイムグラフ上でレンダリングに最も時間がかかっているコンポーネントを特定して、その原因を分析し、
useMemo/useCallbackを使用して最適化を行います。



