跳到正文
cccskills
免费在 GitHub 公开

error-boundary

React error boundary hierarchy, fallback UI patterns, offline-first fallback, retry mechanisms, and graceful degradation.

中文简介正在准备中。当前显示原文说明。

集成工具Sentry
查看安装方法

包含的文件(1)

  • SKILL.md17.7 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Error Boundary Patterns

React error boundary hierarchy and graceful degradation patterns.

Error Boundary Component

// Class-based (React requirement for componentDidCatch)
interface ErrorBoundaryProps {
  children: React.ReactNode
  fallback?: React.ComponentType<FallbackProps>
  onError?: (error: Error, info: React.ErrorInfo) => void
}

interface ErrorBoundaryState {
  error: Error | null
}

export interface FallbackProps {
  error: Error
  reset: () => void
}

export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  state: ErrorBoundaryState = { error: null }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { error }
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('[ErrorBoundary]', error, info.componentStack)
    this.props.onError?.(error, info)
  }

  reset = () => this.setState({ error: null })

  render() {
    if (this.state.error) {
      const Fallback = this.props.fallback ?? DefaultFallback
      return <Fallback error={this.state.error} reset={this.reset} />
    }
    return this.props.children
  }
}

// react-error-boundary library (recommended — battle-tested)
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary'

<ErrorBoundary
  FallbackComponent={MyFallback}
  onError={(error, info) => reportToSentry(error, info)}
  onReset={() => queryClient.clear()}
>
  <App />
</ErrorBoundary>

Error Boundary Hierarchy

App (top-level — catches everything, shows full-page error)
├── Navigation (no boundary — nav errors bubble to app)
├── <ErrorBoundary fallback={PageError}>           ← page level
│   └── DashboardPage
│       ├── <ErrorBoundary fallback={SectionError}>  ← section level
│       │   └── StatsSection
│       └── <ErrorBoundary fallback={WidgetError}>   ← widget level
│           └── ChartWidget                           (one fails, others work)
└── Sidebar (separate boundary — sidebar error ≠ main content error)
// Page-level boundary (reset on navigation)
import { usePathname } from 'next/navigation'
import { ErrorBoundary } from 'react-error-boundary'

export function PageErrorBoundary({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <ErrorBoundary
      key={pathname}              // reset boundary on route change
      FallbackComponent={PageErrorFallback}
      onError={reportError}
    >
      {children}
    </ErrorBoundary>
  )
}

// Widget-level boundary (isolated, inline fallback)
export function WidgetErrorBoundary({ children, name }: { children: React.ReactNode; name: string }) {
  return (
    <ErrorBoundary
      fallback={<WidgetErrorFallback name={name} />}
      onError={(err) => reportError(err, { widget: name })}
    >
      {children}
    </ErrorBoundary>
  )
}

Fallback UI Design Patterns

// Full-page fallback (app boundary)
function PageErrorFallback({ error, reset }: FallbackProps) {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-4 p-8 text-center">
      <h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
      <p className="max-w-md text-gray-500">
        {process.env.NODE_ENV === 'development'
          ? error.message
          : 'An unexpected error occurred. Our team has been notified.'}
      </p>
      <div className="flex gap-3">
        <button onClick={reset} className="btn btn-primary">Try again</button>
        <a href="/" className="btn btn-outline">Go home</a>
      </div>
      {process.env.NODE_ENV === 'development' && (
        <pre className="mt-4 max-w-2xl overflow-auto rounded bg-red-50 p-4 text-left text-sm text-red-800">
          {error.stack}
        </pre>
      )}
    </div>
  )
}

// Inline section fallback (section boundary)
function SectionErrorFallback({ error, reset }: FallbackProps) {
  return (
    <div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
      <p className="text-red-700 font-medium">Failed to load this section</p>
      <button onClick={reset} className="mt-3 text-sm text-red-600 underline hover:no-underline">
        Retry
      </button>
    </div>
  )
}

// Toast-style fallback (non-critical widget)
function WidgetErrorFallback({ name }: { name: string }) {
  return (
    <div className="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700">
      <span>⚠</span>
      <span>{name} unavailable</span>
    </div>
  )
}

Retry Mechanism in Error Boundaries

import { ErrorBoundary } from 'react-error-boundary'
import { useState, useCallback } from 'react'

function RetryFallback({ error, reset }: FallbackProps) {
  const [retries, setRetries] = useState(0)
  const maxRetries = 3

  const handleRetry = useCallback(() => {
    if (retries < maxRetries) {
      setRetries(r => r + 1)
      reset()
    }
  }, [retries, reset])

  return (
    <div className="rounded-lg border bg-white p-6 text-center">
      <p className="font-medium text-gray-900">Failed to load</p>
      {retries < maxRetries ? (
        <button onClick={handleRetry} className="mt-4 btn btn-sm">
          Retry ({maxRetries - retries} left)
        </button>
      ) : (
        <p className="mt-4 text-sm text-gray-400">
          Please refresh the page or <a href="/support" className="underline">contact support</a>.
        </p>
      )}
    </div>
  )
}

Error Reporting (Sentry Integration)

import * as Sentry from '@sentry/nextjs'

export function reportError(
  error: Error,
  context?: Record<string, unknown>,
  info?: React.ErrorInfo
) {
  if (process.env.NODE_ENV === 'development') {
    console.error('[Error]', error, context)
    return
  }

  Sentry.withScope(scope => {
    if (context) scope.setExtras(context)
    if (info?.componentStack) scope.setExtra('componentStack', info.componentStack)
    Sentry.captureException(error)
  })
}

Offline Detection and Fallback UI

'use client'
import { useState, useEffect } from 'react'

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof window !== 'undefined' ? navigator.onLine : true
  )
  useEffect(() => {
    const on  = () => setIsOnline(true)
    const off = () => setIsOnline(false)
    window.addEventListener('online',  on)
    window.addEventListener('offline', off)
    return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off) }
  }, [])
  return isOnline
}

export function OfflineBanner() {
  const isOnline = useOnlineStatus()
  if (isOnline) return null
  return (
    <div role="status" className="fixed top-0 inset-x-0 z-50 bg-amber-500 py-2 text-center text-sm font-medium text-white">
      You are offline. Changes will sync when connection is restored.
    </div>
  )
}

Partial Failure Handling

// Multiple independent widgets — one fails, others keep working
export function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <WidgetErrorBoundary name="Revenue Chart"><RevenueChart /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="User Stats"><UserStats /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="Activity Feed"><ActivityFeed /></WidgetErrorBoundary>
      <WidgetErrorBoundary name="Notifications"><NotificationPanel /></WidgetErrorBoundary>
    </div>
  )
}

// Graceful degradation: show stale data when fetch fails
async function StatsWidget() {
  try {
    const stats = await fetchStats()
    return <StatsDisplay stats={stats} fresh />
  } catch {
    const cached = await getCachedStats()
    if (cached) return <StatsDisplay stats={cached} stale />
    throw new Error('Stats unavailable')   // let error boundary handle
  }
}

Error Recovery (Reset on Navigation)

import { usePathname } from 'next/navigation'
import { useEffect, useRef } from 'react'
import { useErrorBoundary } from 'react-error-boundary'

function NavigationErrorReset() {
  const pathname = usePathname()
  const { resetBoundary } = useErrorBoundary()
  const prevPath = useRef(pathname)

  useEffect(() => {
    if (prevPath.current !== pathname) {
      prevPath.current = pathname
      resetBoundary()
    }
  }, [pathname, resetBoundary])

  return null
}

// Place inside ErrorBoundary to auto-reset on navigation
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <NavigationErrorReset />
  {children}
</ErrorBoundary>

Development vs Production Error Display

function ErrorFallback({ error, reset }: FallbackProps) {
  const isDev = process.env.NODE_ENV === 'development'
  return (
    <div role="alert" className="rounded-lg border border-red-200 bg-red-50 p-6">
      <h2 className="font-semibold text-red-900">
        {isDev ? `Error: ${error.name}` : 'Something went wrong'}
      </h2>
      {isDev ? (
        <details className="mt-3">
          <summary className="cursor-pointer text-sm text-red-700">Stack trace</summary>
          <pre className="mt-2 overflow-auto text-xs text-red-800 whitespace-pre-wrap">{error.stack}</pre>
        </details>
      ) : (
        <p className="mt-2 text-sm text-red-700">An unexpected error occurred. Please try again.</p>
      )}
      <button onClick={reset} className="mt-4 text-sm font-medium text-red-700 underline">Try again</button>
    </div>
  )
}

Testing Error Boundaries

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ErrorBoundary } from 'react-error-boundary'

function BrokenComponent({ shouldThrow }: { shouldThrow: boolean }) {
  if (shouldThrow) throw new Error('Test error')
  return <div>Working fine</div>
}

describe('ErrorBoundary', () => {
  beforeEach(() => vi.spyOn(console, 'error').mockImplementation(() => {}))
  afterEach(() => vi.mocked(console.error).mockRestore())

  it('shows fallback UI when child throws', () => {
    render(
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <BrokenComponent shouldThrow />
      </ErrorBoundary>
    )
    expect(screen.getByRole('alert')).toBeInTheDocument()
    expect(screen.getByText(/something went wrong/i)).toBeInTheDocument()
  })

  it('resets and shows content after retry', async () => {
    const user = userEvent.setup()
    let shouldThrow = true
    function Toggle() {
      if (shouldThrow) throw new Error('Test error')
      return <div>Recovered</div>
    }
    render(<ErrorBoundary FallbackComponent={ErrorFallback}><Toggle /></ErrorBoundary>)
    shouldThrow = false
    await user.click(screen.getByRole('button', { name: /try again/i }))
    expect(screen.getByText('Recovered')).toBeInTheDocument()
  })
})

Decision Matrix

ScopeFallback TypeWhen
WidgetInline placeholderNon-critical UI section
SectionCollapsed + retry buttonIndependent feature area
PageFull page with retryPage-level data failure
AppFull page with refreshUnrecoverable state
NetworkToast + offline bannerConnectivity issues

Original Patterns (preserved for reference)

Basic Error Boundary (react-error-boundary)

import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert" className="p-4 bg-red-50 border border-red-200 rounded">
      <h2 className="text-lg font-semibold text-red-800">Something went wrong</h2>
      <pre className="mt-2 text-sm text-red-600">{error.message}</pre>
      <button
        onClick={resetErrorBoundary}
        className="mt-3 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
      >
        Try again
      </button>
    </div>
  );
}

// Usage
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <RiskyComponent />
</ErrorBoundary>

Error Boundary Hierarchy

App Error Boundary (full page fallback)
├── Layout Error Boundary (layout-level recovery)
│   ├── Sidebar Error Boundary (sidebar collapses gracefully)
│   └── Main Content Error Boundary
│       ├── Widget A Error Boundary (individual widget fails)
│       ├── Widget B Error Boundary
│       └── Widget C Error Boundary
// App-level: full page fallback with navigation
<ErrorBoundary FallbackComponent={FullPageError}>
  <Layout>
    {/* Section-level: isolated failures */}
    <ErrorBoundary FallbackComponent={SectionError}>
      <Dashboard />
    </ErrorBoundary>
    <ErrorBoundary FallbackComponent={SectionError}>
      <Analytics />
    </ErrorBoundary>
  </Layout>
</ErrorBoundary>

Next.js App Router (error.tsx)

// app/dashboard/error.tsx
'use client';

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to error reporting service
    reportError(error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2>Dashboard could not load</h2>
      <p className="text-gray-500">{error.message}</p>
      <button onClick={reset} className="mt-4 btn-primary">
        Retry
      </button>
    </div>
  );
}

Fallback UI Patterns

// 1. Inline fallback (widget level)
function InlineFallback({ error }: { error: Error }) {
  return (
    <div className="p-3 text-sm text-gray-500 bg-gray-50 rounded">
      Unable to load this section
    </div>
  );
}

// 2. Toast notification (non-critical)
function ToastFallback({ error, resetErrorBoundary }: FallbackProps) {
  useEffect(() => {
    toast.error(error.message, {
      action: { label: 'Retry', onClick: resetErrorBoundary },
    });
  }, [error]);
  return null; // renders nothing, shows toast instead
}

// 3. Full page (critical)
function FullPageFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-2xl font-bold">Something went wrong</h1>
      <p className="mt-2 text-gray-600">Please try refreshing the page</p>
      <div className="mt-4 space-x-3">
        <button onClick={resetErrorBoundary}>Retry</button>
        <button onClick={() => window.location.reload()}>Refresh</button>
      </div>
    </div>
  );
}

Retry with Error Boundary

import { ErrorBoundary } from 'react-error-boundary';
import { QueryErrorResetBoundary } from '@tanstack/react-query';

// Auto-reset on navigation
<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onReset={() => {
    // Clear any cached state that caused the error
    queryClient.invalidateQueries();
  }}
  resetKeys={[pathname]} // auto-reset when URL changes
>
  <Outlet />
</ErrorBoundary>

// With TanStack Query
<QueryErrorResetBoundary>
  {({ reset }) => (
    <ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
      <SuspenseComponent />
    </ErrorBoundary>
  )}
</QueryErrorResetBoundary>

Error Reporting

function logError(error: Error, info: { componentStack?: string }) {
  // Send to Sentry/monitoring
  if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
    Sentry.captureException(error, {
      extra: { componentStack: info.componentStack },
    });
  }
}

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={logError}
>
  <App />
</ErrorBoundary>

Offline Detection

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  );

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Offline banner
function OfflineBanner() {
  const isOnline = useOnlineStatus();
  if (isOnline) return null;

  return (
    <div className="fixed top-0 inset-x-0 bg-yellow-500 text-center py-2 z-50">
      You are offline. Some features may be unavailable.
    </div>
  );
}

Testing Error Boundaries

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

function ThrowError() {
  throw new Error('Test error');
}

test('renders fallback on error', () => {
  const spy = vi.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByRole('alert')).toBeInTheDocument();
  expect(screen.getByText(/test error/i)).toBeInTheDocument();

  spy.mockRestore();
});

test('resets on retry click', async () => {
  let shouldThrow = true;
  function MaybeThrow() {
    if (shouldThrow) throw new Error('Boom');
    return <div>Recovered</div>;
  }

  render(
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MaybeThrow />
    </ErrorBoundary>
  );

  shouldThrow = false;
  await userEvent.click(screen.getByText('Try again'));
  expect(screen.getByText('Recovered')).toBeInTheDocument();
});

Decision Matrix

ScopeFallback TypeWhen
WidgetInline placeholderNon-critical UI section
SectionCollapsed + retry buttonIndependent feature area
PageFull page with retryPage-level data failure
AppFull page with refreshUnrecoverable state
NetworkToast + offline bannerConnectivity issues

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

WCAG 2.2 AA compliance, ARIA patterns, keyboard navigation, screen reader optimization

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

axe-core integration, WCAG 2.2 AA checklist, keyboard navigation testing, screen reader testing, and ARIA pattern validation.

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

Steam-style achievement system with XP, levels, streaks, and skill trees. Gamifies the development workflow. 25 achievements across 5 categories.

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

Framework for measuring and tracking agent response quality over time. Detects regressions before they reach production. Use when evaluating agent changes, auditing quality, or establishing performance baselines.

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

Agent Context Isolation

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

Agent ve skill dosyalarinin yapisal dogrulamasi. Frontmatter kontrol, naming convention, zorunlu bolum kontrolu, tutarlilik denetimi. Yeni agent/skill eklendiginde veya mevcut dosyalar duzenlediginde otomatik calistirilir.

中文简介正在准备中。当前显示原文说明。

vibeeval/vibecosystem5382026年8月9日 更新

查看 vibeeval 的全部技能

报告此技能的问题