طراحی خود Fallback UI

در درس قبل دیدیم چگونه با Error Boundary، اثر یک خطا را به بخشی مشخص از صفحه محدود کنیم. اما چیزی که به‌جای آن بخش نمایش داده می‌شود، یعنی Fallback UI، خودش یک تصمیم طراحی مهم است. در این درس، به جزئیات بیشتری درباره طراحی و پیاده‌سازی این بخش می‌پردازیم.

یادآوری: Fallback از کجا می‌آید

در Error Boundary که در درس قبل ساختیم، وقتی hasError برابر true شود، به‌جای this.props.children، یک JSX دیگر رندر می‌شود؛ همان Fallback UI:

jsx
render() {
  if (this.state.hasError) {
    return <p>مشکلی پیش آمد.</p>;
  }
  return this.props.children;
}

مشکل یک Fallback ثابت و یکسان در سراسر برنامه

اگر همیشه از همان یک Error Boundary، با همان یک پیام ثابت، در سراسر برنامه استفاده کنیم، تجربه کاربری ضعیفی می‌سازیم: پیام «مشکلی پیش آمد» برای یک ویجت کوچک آب‌وهوا، همان‌قدر مناسب است که برای یک بخش حیاتی مثل پرداخت؛ در حالی که این دو موقعیت، نیاز به واکنش کاملاً متفاوتی دارند.

پذیرفتن Fallback به‌عنوان Prop

برای قابل‌استفاده مجدد شدن، معمولاً Error Boundary طوری نوشته می‌شود که خود Fallback را از بیرون، به‌عنوان یک Prop دریافت کند:

jsx
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

حالا هر جایی که این کامپوننت استفاده می‌شود، می‌تواند Fallback متناسب با همان بخش را تعیین کند:

jsx
<ErrorBoundary fallback={<p>آب‌وهوا در دسترس نیست.</p>}>
  <WeatherWidget />
</ErrorBoundary>

<ErrorBoundary fallback={<PaymentErrorPage />}>
  <PaymentForm />
</ErrorBoundary>

یک گزینه بازیابی: دکمه «تلاش دوباره»

یک Fallback خوب، گاهی فقط یک پیام نیست؛ راهی هم برای ادامه کار کاربر می‌دهد. می‌توان یک متد برای بازنشانی State خطا اضافه کرد:

jsx
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
    this.handleRetry = this.handleRetry.bind(this);
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  handleRetry() {
    this.setState({ hasError: false });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>مشکلی پیش آمد.</p>
          <button onClick={this.handleRetry}>تلاش دوباره</button>
        </div>
      );
    }
    return this.props.children;
  }
}

با کلیک روی «تلاش دوباره»، hasError به false برمی‌گردد و React دوباره سعی می‌کند children را Render کند. اگر علت خطا موقتی بوده (مثلاً یک مقدار ناپایدار که بعداً درست شده)، این بار ممکن است بدون مشکل رندر شود.

ثبت خطا برای بررسی بعدی

علاوه بر نمایش Fallback، معمولاً می‌خواهیم از وقوع خطا باخبر شویم، نه فقط کاربر. متد دیگری به نام componentDidCatch دقیقاً برای همین منظور است و می‌تواند جزئیات خطا را به یک سرویس گزارش خطا ارسال کند:

jsx
componentDidCatch(error, info) {
  logErrorToService(error, info.componentStack);
}

مثال قابل اجرا

در این مثال، یک Fallback اختصاصی همراه با دکمه تلاش دوباره ساخته شده است.

Error Boundary با Fallback سفارشی و دکمه تلاش دوباره
import { Component, useState } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
    this.handleRetry = this.handleRetry.bind(this);
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  handleRetry() {
    this.setState({ hasError: false });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          {this.props.fallback}
          <button onClick={this.handleRetry}>تلاش دوباره</button>
        </div>
      );
    }
    return this.props.children;
  }
}

function Widget({ broken }) {
  if (broken) throw new Error("خطای فرضی");
  return <p>همه‌چیز درست است.</p>;
}

function App() {
  const [broken, setBroken] = useState(true);

  return (
    <div>
      <button onClick={() => setBroken(false)}>رفع مشکل داده (قبل از تلاش دوباره بزنید)</button>
      <ErrorBoundary fallback={<p>این ویجت موقتاً در دسترس نیست.</p>}>
        <Widget broken={broken} />
      </ErrorBoundary>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Fallback UI نباید همیشه یک پیام ثابت و یکسان باشد؛ بهتر است از بیرون، به‌عنوان Prop، و متناسب با اهمیت هر بخش طراحی شود، و در صورت امکان راهی برای تلاش دوباره هم داشته باشد. متد componentDidCatch، جایی مناسب برای ثبت و گزارش خطا به یک سرویس بیرونی است. در درس بعدی، به مدیریت خطاهای Async، که Error Boundary آن‌ها را نمی‌گیرد، می‌پردازیم.