404 Not Found

404 Not Found


nginx

エラーの境界とデバッグ

トムはEコマースのバックエンドを担当していたが、本番サイトで「ホワイトスクリーン」の問題が発生した。ユーザーからは、注文詳細ページを開くと画面が完全に真っ白になるという報告があり、コンソールにはエラー Cannot read properties of undefined (reading 'name') が表示されていた。原因は、レンダリングフェーズ中に子コンポーネントによってスローされた、キャッチされていないJavaScriptエラーであり、これがReactコンポーネントツリー全体のクラッシュを引き起こしていました。トムは、個々のコンポーネントの障害がページ全体の使いやすさに影響を与えないよう、アプリケーション内にエラーバウンダリーシステムを構築する必要があります。


1. 学習内容


2. 概念図

以下の図は、エラー境界の捕捉プロセスとプロファイラの分析チェーンを示しています:

100%
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 開発中のデバッグ トラブルシューティング パフォーマンス・レンダリングに関する問題のトラブルシューティング

トムの注文詳細ページは、以下の要素で構成されています:

TEXT
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 コンポーネント

TSX
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の活用

TSX
// 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 を表示するだけでは不十分な場合があり、ユーザーが特定のデータを更新する必要があることもあります。以下に、「再試行」機能を備えたエラーバウンダリの使用例を示します。

TSX
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 ブロックで囲む
エラー・バウンダリー自身のエラー それ自体ではキャッチできないエラーをスローする 最外層で別のエラー・バウンダリーで囲む

非同期コードにおけるイベント処理と適切なエラー処理

TSX
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」タブは、コンポーネントのレンダリングパフォーマンスを分析するための主要なツールです。このタブでは、各コンポーネントのレンダリング時間を視覚的に示す「フレイムグラフ」が生成されます。

使用方法

TEXT
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

フレイムチャートの読み方

TEXT
┌────────────────────────────────────────────┐
│  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>コンポーネントは、コード内の特定のコンポーネントのレンダリング時間を正確に測定できるため、パフォーマンス指標の自動監視に適しています:

TSX
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>
  )
}
▶ 試してみよう

一般的なパフォーマンス最適化の手法

TSX
// 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 コンポーネント名を入力
ソースコードへ コンポーネントの実装を表示 <> アイコンをクリック
TEXT
// 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 リクエストのエラー処理 — リトライ機能を用いたデータの取得

JSX
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 との連携

JSX
// 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>
}
▶ 試してみよう

❓ よくある質問

Q エラーバウンダリーはすべてのエラーを捕捉できますか?
A いいえ。エラーバウンダリーが捕捉できるのは、レンダリングフェーズ中、ライフサイクルメソッド内、およびコンストラクタ内で発生するエラーのみです。以下の4種類のエラーは捕捉できません:イベントハンドラ内のエラー(try/catchを使用)、非同期コード内のエラー(Promise.catchを使用)、サーバーサイドレンダリング中のエラー、およびエラーバウンダリ内部のエラーです。エラー処理を設計する際には、try/catchとエラーバウンダリを組み合わせて、階層的なアプローチを採用する必要があります。
Q プロファイラーのフレームグラフにおける「灰色」と「青色」は何を意味しますか?
A 灰色は、そのコンポーネントがこのコミット中に再レンダリングされなかったことを示します(パフォーマンス良好)。青は、コンポーネントが再レンダリングされたものの、所要時間が通常の範囲内であったことを示します。黄色または赤は、レンダリングに時間がかかり、最適化が必要であることを示します。目標は、インタラクション中にほとんどのコンポーネントを灰色または水色に保つことです。
Q React.memo はすべてのコンポーネントを自動的に最適化しますか?
A いいえ。React.memo は浅い比較のみを行います。propsにオブジェクトや配列が含まれている場合、レンダリングのたびに新しい参照が生成され、メモ化が失敗します。この場合、参照を安定させるためにuseMemo / useCallbackを使用するか、React.memoに2番目の引数(カスタム比較関数React.memo(Comp, (prev, next) => deepEqual(prev, next)))を渡す必要があります。
Q <Profiler> コンポーネントは本番環境のパフォーマンスに影響を与えますか?
A Reactの公式ドキュメントによると、<Profiler> コンポーネントは本番環境でわずかなパフォーマンスのオーバーヘッドを引き起こします。開発環境でのみ使用するか、環境変数 {process.env.NODE_ENV === 'development' && <Profiler>...} を通じて制御することをお勧めします。本番環境でパフォーマンスの監視が必要な場合は、専用のパフォーマンス監視ライブラリ(web-vitalsなど)や、Sentryのパフォーマンストレース機能の利用を検討してください。
Q エラーバウンダリーを関数コンポーネントとして記述することはできますか?
A 現時点ではできません。エラーバウンダリーは、getDerivedStateFromErrorcomponentDidCatch という 2 つのライフサイクルメソッドに依存していますが、これらはクラスコンポーネントでのみサポートされています。Reactチームは、将来的にフック版が提供される可能性を示唆していますが、現時点(React 18/19)では、クラスコンポーネントを使用してのみ実装可能です。クラスベースのエラーバウンダリコンポーネントを作成し、それを関数コンポーネントでラップすることで、エラー回復ロジックを処理することができます。

📖 まとめ


📝 練習問題

  1. ErrorBoundary コンポーネントを作成し、OrderPage: Wrap OrderHeader, OrderItems, and PaymentInfo in separate ErrorBoundary コンポーネント内で階層的に使用します。手動でレンダリングエラー(誤ったプロパティの渡しかたなど)を発生させ、影響を受けた領域のみにフォールバック UI が表示され、インターフェースのその他の部分は正常にレンダリングされることを確認します。
  2. Profiler コンポーネントを使用して、100個のリスト項目を含むコンポーネントのレンダリング時間を測定します。React.memo による最適化を行った後、再度レンダリング時間を測定し、2回の測定結果の差を actualDuration で比較して、最適化の効果を確認します。
  3. ChromeでReact DevToolsのプロファイラーを開き、ページの操作(検索、フィルタリング、ソートなど)を記録し、フレイムグラフ上でレンダリングに最も時間がかかっているコンポーネントを特定して、その原因を分析し、useMemo / useCallback を使用して最適化を行います。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%