Error Boundary در React: صفحهٔ سفید را متوقف کنید

Error Boundary در React: صفحهٔ سفید را متوقف کنید
در این مقاله می‌خوانید
  1. چرا صفحه سفید می‌شود
  2. کامل‌ترین شکل
  3. استفاده
  4. چه چیزی را نمی‌گیرد
  5. معماری مرزها
  6. بازنشانی خودکار هنگام تغییر صفحه
  7. چطور تستش کنیم
  8. اشتباهات رایج
  9. نسخهٔ آماده
  10. ثبت کردن را فراموش نکنید
  11. منابع و مطالعهٔ بیشتر

بدترین حالت شکست در یک اپلیکیشن React، صفحهٔ سفید است. نه پیام خطایی، نه دکمهٔ تلاش دوباره، نه راهی برای فهمیدن اینکه چه شد. کاربر فقط صفحهٔ خالی می‌بیند و می‌رود.

مرز خطا مکانیزمی است که این را متوقف می‌کند.

چرا صفحه سفید می‌شود

از نسخهٔ ۱۶ به بعد، React اگر خطایی حین رندر رخ دهد و کسی نگیردش، کل درخت را از DOM حذف می‌کند. منطقش این است که رابط کاربریِ نیمه‌خراب می‌تواند دادهٔ غلط نشان دهد یا کاربر را به عمل اشتباهی بکشاند — و هیچ‌چیز نشان دادن، از آن امن‌تر است. رایج‌ترین این خطاها Cannot read properties of undefined است.

این تصمیم درستی است، ولی فقط اگر شما مرزی گذاشته باشید که جلوی گسترشش را بگیرد.

کامل‌ترین شکل

class ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, info) {
    reportError(error, { componentStack: info.componentStack });
  }

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

  render() {
    if (this.state.error) {
      return this.props.fallback
        ? this.props.fallback(this.state.error, this.reset)
        : <DefaultError onRetry={this.reset} />;
    }
    return this.props.children;
  }
}

سه نکته در این پیاده‌سازی که در نسخه‌های ساده‌تر نیست:

خودِ خطا در state نگه داشته می‌شود، نه فقط یک بولین. برای نمایش پیام مناسب و برای ثبت، به آن نیاز دارید.

تابع reset به کاربر اجازه می‌دهد دوباره تلاش کند بدون رفرش کل صفحه. بدون این، تنها راه خروج از حالت خطا، بارگذاری مجدد است.

componentStack ثبت می‌شود. این چیزی است که می‌گوید خطا در کدام کامپوننت رخ داد — معمولاً مفیدتر از stack جاوااسکریپت، چون سلسله‌مراتب رابط کاربری را نشان می‌دهد. بدون Source Map، همین هم به نام‌های فشرده اشاره می‌کند.

استفاده

<ErrorBoundary fallback={(err, retry) => (
  <div className="err">
    <p>نمایش این بخش ممکن نشد.</p>
    <button onClick={retry}>تلاش دوباره</button>
  </div>
)}>
  <Comments postId={id} />
</ErrorBoundary>

چه چیزی را نمی‌گیرد

مهم‌ترین بخش این مقاله، چون سوءتفاهم دربارهٔ آن رایج است. مرز خطا این چهار دسته را نمی‌گیرد:

  • خطای هندلر رویداد. کلیک روی دکمه‌ای که خطا می‌دهد، خارج از چرخهٔ رندر است. try/catch لازم دارید.
  • کد ناهمگام. خطای داخل setTimeout، fetch یا useEffect پس از پایان رندر رخ می‌دهد.
  • رندر سمت سرور. مرزها فقط در مرورگر کار می‌کنند.
  • خطای خود مرز. اگر fallback شما خطا بدهد، به مرز بالاتر می‌رود. برای همین رابط جایگزین باید تا حد ممکن ساده باشد — بدون فراخوانی داده، بدون منطق پیچیده.

نتیجهٔ عملی: مرز خطا یک لایه از چند لایه است، نه راه‌حل کامل مدیریت خطا. لایه‌های دیگر را در مدیریت خطا در React مرور کرده‌ایم.

معماری مرزها

گذاشتن یک مرز دور کل برنامه، تقریباً هیچ چیزی حل نمی‌کند — همان صفحهٔ سفید را می‌گیرید با ظاهر بهتر. الگویی که واقعاً کار می‌کند سه سطح دارد:

سطح برنامه: آخرین خط دفاع. صفحه‌ای با پیام و دکمهٔ بارگذاری مجدد.

سطح مسیر: اگر صفحه‌ای شکست، ناوبری و هدر سالم بمانند تا کاربر بتواند جای دیگری برود.

سطح ویجت: مهم‌ترین سطح. هر بخشی که داده‌ای مستقل می‌گیرد — نظرات، محصولات پیشنهادی، نمودار — مرز خودش را داشته باشد. اگر یکی شکست، بقیهٔ صفحه کار می‌کند.

تفاوت عملی این است: در صفحهٔ محصول، اگر بخش «محصولات مشابه» خطا بدهد، کاربر همچنان می‌تواند همان محصول را بخرد.

بازنشانی خودکار هنگام تغییر صفحه

مشکل ظریفی که تیم‌ها دیر متوجه می‌شوند: وقتی مرز به حالت خطا می‌رود، آنجا می‌ماند — حتی اگر کاربر به صفحهٔ دیگری برود، چون کامپوننت همچنان mount است.

راه‌حل، وادار کردن React به ساختن نمونهٔ تازه با تغییر key است:

<ErrorBoundary key={location.pathname}>
  <Routes />
</ErrorBoundary>

با هر تغییر مسیر، مرز از نو ساخته می‌شود و حالت خطای قبلی پاک می‌شود.

چطور تستش کنیم

مرز خطایی که تست نشده، معمولاً در لحظهٔ واقعی کار نمی‌کند. ساده‌ترین راه، کامپوننتی است که عمداً خطا می‌دهد:

function Boom({ when }) {
  if (when) throw new Error('تست مرز خطا');
  return <p>سالم</p>;
}

// در تست:
render(
  <ErrorBoundary fallback={() => <p>خطا رخ داد</p>}>
    <Boom when={true} />
  </ErrorBoundary>
);
expect(screen.getByText('خطا رخ داد')).toBeInTheDocument();

نکته‌ای که آزاردهنده است: React حتی وقتی مرز خطا را می‌گیرد، خطا را در کنسول هم چاپ می‌کند. در تست‌ها این خروجی را نویز می‌کند؛ می‌توانید موقتاً console.error را در همان تست ساکت کنید.

اشتباهات رایج

fallback پیچیده. اگر رابط جایگزین شما خودش داده می‌گیرد یا منطق دارد، می‌تواند خطا بدهد — و آن خطا به مرز بالاتر می‌رود، یعنی همان صفحهٔ سفیدی که می‌خواستید جلویش را بگیرید. رابط جایگزین باید تا حد ممکن ساده و ایستا باشد.

ثبت نکردن. پرتکرارترین اشتباه و در بخش بعد مفصل‌تر گفته شده.

پیام فنی به کاربر. نمایش متن خام خطا به کاربر نه مفید است و نه امن — گاهی مسیر فایل یا ساختار داخلی را لو می‌دهد. پیام عمومی به کاربر، جزئیات کامل به لاگ.

مرز بدون راه خروج. اگر کاربر گیر کند و تنها راهش رفرش کل صفحه باشد، بخشی از ارزش را از دست داده‌اید. دکمهٔ تلاش دوباره تقریباً همیشه ارزشش را دارد.

نسخهٔ آماده

اگر نمی‌خواهید خودتان بنویسید، کتابخانهٔ react-error-boundary نسخهٔ بالغی از همین الگو را می‌دهد، به‌همراه هوک‌هایی برای انداختن خطا از کد ناهمگام به نزدیک‌ترین مرز — که یکی از محدودیت‌های اصلی مرزها را دور می‌زند.

ثبت کردن را فراموش نکنید

خطری که در مرز خطا هست: می‌توانید صفحهٔ سفید را با پیامی زیبا جایگزین کنید و هرگز نفهمید که این اتفاق می‌افتد. از دید شما همه‌چیز خوب است؛ از دید کاربر، بخشی از سایت هیچ‌وقت کار نمی‌کند.

این بدتر از صفحهٔ سفید است، چون صفحهٔ سفید دست‌کم باعث می‌شود کسی زنگ بزند.

پس componentDidCatch باید همیشه خطا را جایی بفرستد که واقعاً نگاهش می‌کنید، همراه با componentStack و اطلاعات کاربر. مرز خطا برای کاربر است؛ ثبت کردن برای شما.

منابع و مطالعهٔ بیشتر

باگ‌ماگ را رایگان امتحان کنید

خطاهای سایتتان را خودکار ثبت کنید و بگذارید کاربران با یک کلیک باگ گزارش دهند.

شروع رایگان