بیاموزید چگونه از ErrorBoundary در React برای مدیریت خطاها، جلوگیری از کرش کردن برنامه و ارائه تجربه کاربری بهتر با استراتژیهای بازیابی قوی استفاده کنید.
React ErrorBoundary: استراتژیهای جداسازی و بازیابی خطا
در دنیای پویای توسعه فرانتاند، بهویژه هنگام کار با فریمورکهای پیچیده مبتنی بر کامپوننت مانند ریاکت، خطاهای غیرمنتظره اجتنابناپذیر هستند. این خطاها، اگر به درستی مدیریت نشوند، میتوانند منجر به کرش کردن برنامه و تجربه کاربری ناخوشایند شوند. کامپوننت ErrorBoundary در ریاکت یک راهحل قوی برای مدیریت زیبا و کارآمد این خطاها، جداسازی آنها و ارائه استراتژیهای بازیابی ارائه میدهد. این راهنمای جامع، قدرت ErrorBoundary را بررسی میکند و نشان میدهد چگونه آن را به طور مؤثر برای ساخت برنامههای ریاکت مقاومتر و کاربرپسندتر برای مخاطبان جهانی پیادهسازی کنید.
درک نیاز به Error Boundary ها
قبل از پرداختن به پیادهسازی، بیایید بفهمیم چرا error boundary ها ضروری هستند. در ریاکت، خطاهایی که هنگام رندرینگ، در متدهای چرخه حیات (lifecycle methods) یا در سازندههای (constructors) کامپوننتهای فرزند رخ میدهند، میتوانند به طور بالقوه کل برنامه را کرش کنند. این به این دلیل است که خطاهای مدیریتنشده به سمت بالای درخت کامپوننتها منتشر میشوند و اغلب منجر به یک صفحه سفید یا یک پیام خطای غیرمفید میشوند. تصور کنید کاربری در ژاپن در حال انجام یک تراکنش مالی مهم است و ناگهان به دلیل یک خطای جزئی در یک کامپوننت به ظاهر نامرتبط، با یک صفحه سفید مواجه میشود. این موضوع نیاز حیاتی به مدیریت فعالانه خطا را نشان میدهد.
Error boundary ها راهی برای گرفتن خطاهای جاوااسکریپت در هر جای درخت کامپوننتهای فرزندشان، لاگ کردن آن خطاها و نمایش یک UI جایگزین (fallback UI) به جای کرش کردن درخت کامپوننت فراهم میکنند. آنها به شما اجازه میدهند کامپوننتهای معیوب را جدا کرده و از تأثیر خطاهای یک بخش از برنامه بر بخشهای دیگر جلوگیری کنید و تجربهای پایدارتر و قابل اعتمادتر را در سطح جهانی برای کاربر تضمین کنید.
React ErrorBoundary چیست؟
یک ErrorBoundary کامپوننتی در ریاکت است که خطاهای جاوااسکریپت را در هر جای درخت کامپوننتهای فرزندش میگیرد، آن خطاها را لاگ میکند و یک UI جایگزین نمایش میدهد. این یک کامپوننت کلاسی است که یک یا هر دو متد چرخه حیات زیر را پیادهسازی میکند:
static getDerivedStateFromError(error): این متد چرخه حیات پس از پرتاب شدن یک خطا توسط یک کامپوننت فرزند فراخوانی میشود. این متد خطای پرتاب شده را به عنوان آرگومان دریافت میکند و باید مقداری را برای بهروزرسانی استیت کامپوننت برگرداند.componentDidCatch(error, info): این متد چرخه حیات پس از پرتاب شدن یک خطا توسط یک کامپوننت فرزند فراخوانی میشود. این متد دو آرگومان دریافت میکند: خطای پرتاب شده و یک شیء info که حاوی اطلاعاتی در مورد اینکه کدام کامپوننت خطا را پرتاب کرده است، میباشد. میتوانید از این متد برای لاگ کردن اطلاعات خطا یا انجام سایر عملیات جانبی (side effects) استفاده کنید.
ایجاد یک کامپوننت ErrorBoundary پایه
بیایید یک کامپوننت ErrorBoundary پایه برای نشان دادن اصول اساسی ایجاد کنیم.
مثال کد
در اینجا کد یک کامپوننت ساده ErrorBoundary آمده است:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
// استیت را بهروزرسانی کنید تا رندر بعدی UI جایگزین را نمایش دهد.
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// مثال "componentStack":
// in ComponentThatThrows (created by App)
// in App
console.error("Caught an error:", error);
console.error("Error info:", info.componentStack);
this.setState({ error: error, errorInfo: info });
// همچنین میتوانید خطا را به یک سرویس گزارش خطا لاگ کنید
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// میتوانید هر UI جایگزین سفارشی را رندر کنید
return (
Something went wrong.
Error: {this.state.error && this.state.error.toString()}
{this.state.errorInfo && this.state.errorInfo.componentStack}
);
}
return this.props.children;
}
}
export default ErrorBoundary;
توضیح
- سازنده (Constructor): سازنده، استیت کامپوننت را با
hasErrorبرابر باfalseمقداردهی اولیه میکند. ما همچنین error و errorInfo را برای اهداف دیباگینگ ذخیره میکنیم. getDerivedStateFromError(error): این متد استاتیک زمانی که خطایی توسط یک کامپوننت فرزند پرتاب میشود، فراخوانی میشود. این متد استیت را بهروزرسانی میکند تا نشان دهد که خطایی رخ داده است.componentDidCatch(error, info): این متد پس از پرتاب شدن خطا فراخوانی میشود. این متد خطا و یک شیءinfoحاوی اطلاعاتی در مورد پشته کامپوننت (component stack) را دریافت میکند. در اینجا، ما خطا را در کنسول لاگ میکنیم (میتوانید آن را با مکانیزم لاگینگ دلخواه خود مانند Sentry، Bugsnag یا یک راهحل داخلی سفارشی جایگزین کنید). ما همچنین error و errorInfo را در استیت تنظیم میکنیم.render(): متد render، استیتhasErrorرا بررسی میکند. اگرtrueباشد، یک UI جایگزین را رندر میکند؛ در غیر این صورت، فرزندان کامپوننت را رندر میکند. UI جایگزین باید آموزنده و کاربرپسند باشد. گنجاندن جزئیات خطا و پشته کامپوننت، اگرچه برای توسعهدهندگان مفید است، اما باید به دلایل امنیتی در محیطهای پروداکشن به صورت شرطی رندر شود یا حذف گردد.
استفاده از کامپوننت ErrorBoundary
برای استفاده از کامپوننت ErrorBoundary، کافی است هر کامپوننتی را که ممکن است خطا ایجاد کند، درون آن قرار دهید.
مثال کد
import ErrorBoundary from './ErrorBoundary';
function MyComponent() {
return (
{/* کامپوننتهایی که ممکن است خطا ایجاد کنند */}
);
}
function App() {
return (
);
}
export default App;
توضیح
در این مثال، MyComponent با ErrorBoundary پوشانده شده است. اگر هر خطایی در MyComponent یا فرزندان آن رخ دهد، ErrorBoundary آن را گرفته و UI جایگزین را رندر میکند.
استراتژیهای پیشرفته ErrorBoundary
در حالی که ErrorBoundary پایه یک سطح اساسی از مدیریت خطا را فراهم میکند، چندین استراتژی پیشرفته وجود دارد که میتوانید برای بهبود مدیریت خطای خود پیادهسازی کنید.
۱. Error Boundary های جزئی (Granular)
به جای پوشاندن کل برنامه با یک ErrorBoundary واحد، استفاده از error boundary های جزئی را در نظر بگیرید. این کار شامل قرار دادن کامپوننتهای ErrorBoundary در اطراف بخشهای خاصی از برنامه شماست که بیشتر مستعد خطا هستند یا جایی که شکست تأثیر محدودی خواهد داشت. به عنوان مثال، میتوانید ویجتهای جداگانه یا کامپوننتهایی را که به منابع داده خارجی متکی هستند، بپوشانید.
مثال
function ProductList() {
return (
{/* لیست محصولات */}
);
}
function RecommendationWidget() {
return (
{/* موتور پیشنهاددهنده */}
);
}
function App() {
return (
);
}
در این مثال، RecommendationWidget دارای ErrorBoundary مخصوص به خود است. اگر موتور پیشنهاددهنده با شکست مواجه شود، بر ProductList تأثیری نخواهد گذاشت و کاربر همچنان میتواند محصولات را مرور کند. این رویکرد جزئی با جداسازی خطاها و جلوگیری از گسترش آنها در سراسر برنامه، تجربه کلی کاربر را بهبود میبخشد.
۲. لاگ و گزارش خطا
لاگ کردن خطاها برای دیباگینگ و شناسایی مشکلات تکراری حیاتی است. متد چرخه حیات componentDidCatch مکان ایدهآلی برای ادغام با سرویسهای لاگ خطا مانند Sentry، Bugsnag یا Rollbar است. این سرویسها گزارشهای خطای دقیقی شامل ردپای پشته (stack traces)، زمینه کاربر و اطلاعات محیط را ارائه میدهند که به شما امکان تشخیص و حل سریع مشکلات را میدهد. برای اطمینان از انطباق با مقررات حریم خصوصی مانند GDPR، قبل از ارسال لاگهای خطا، دادههای حساس کاربر را ناشناس یا حذف کنید.
مثال
import * as Sentry from "@sentry/react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError(error) {
// استیت را بهروزرسانی کنید تا رندر بعدی UI جایگزین را نمایش دهد.
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// خطا را به Sentry لاگ کنید
Sentry.captureException(error, { extra: info });
// همچنین میتوانید خطا را به یک سرویس گزارش خطا لاگ کنید
console.error("Caught an error:", error);
}
render() {
if (this.state.hasError) {
// میتوانید هر UI جایگزین سفارشی را رندر کنید
return (
Something went wrong.
);
}
return this.props.children;
}
}
export default ErrorBoundary;
در این مثال، متد componentDidCatch از Sentry.captureException برای گزارش خطا به Sentry استفاده میکند. میتوانید Sentry را طوری پیکربندی کنید که به تیم شما اعلان ارسال کند و به شما امکان پاسخ سریع به خطاهای حیاتی را بدهد.
۳. UI جایگزین سفارشی
UI جایگزین که توسط ErrorBoundary نمایش داده میشود، فرصتی برای ارائه یک تجربه کاربرپسند حتی در هنگام وقوع خطا است. به جای نمایش یک پیام خطای عمومی، نمایش یک پیام آموزندهتر که کاربر را به سمت یک راهحل راهنمایی میکند، در نظر بگیرید. این ممکن است شامل دستورالعملهایی در مورد نحوه رفرش کردن صفحه، تماس با پشتیبانی یا تلاش مجدد در زمان دیگری باشد. همچنین میتوانید UI جایگزین را بر اساس نوع خطای رخ داده سفارشی کنید.
مثال
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error) {
// استیت را بهروزرسانی کنید تا رندر بعدی UI جایگزین را نمایش دهد.
return {
hasError: true,
error: error,
};
}
componentDidCatch(error, info) {
console.error("Caught an error:", error);
// همچنین میتوانید خطا را به یک سرویس گزارش خطا لاگ کنید
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// میتوانید هر UI جایگزین سفارشی را رندر کنید
if (this.state.error instanceof NetworkError) {
return (
Network Error
Please check your internet connection and try again.
);
} else {
return (
Something went wrong.
Please try refreshing the page or contact support.
);
}
}
return this.props.children;
}
}
export default ErrorBoundary;
در این مثال، UI جایگزین بررسی میکند که آیا خطا از نوع NetworkError است یا خیر. اگر چنین باشد، پیام خاصی را نمایش میدهد که به کاربر دستور میدهد اتصال اینترنت خود را بررسی کند. در غیر این صورت، یک پیام خطای عمومی نمایش میدهد. ارائه راهنماییهای خاص و عملی میتواند تجربه کاربری را به شدت بهبود بخشد.
۴. مکانیزمهای تلاش مجدد (Retry)
در برخی موارد، خطاها موقتی هستند و با تلاش مجدد عملیات قابل حل هستند. میتوانید یک مکانیزم تلاش مجدد در ErrorBoundary پیادهسازی کنید تا عملیات ناموفق را به طور خودکار پس از یک تأخیر مشخص دوباره امتحان کند. این میتواند به ویژه برای مدیریت خطاهای شبکه یا قطعیهای موقت سرور مفید باشد. در مورد پیادهسازی مکانیزمهای تلاش مجدد برای عملیاتی که ممکن است عوارض جانبی داشته باشند، محتاط باشید، زیرا تلاش مجدد آنها میتواند منجر به عواقب ناخواسته شود.
مثال
import React, { useState, useEffect } from 'react';
function DataFetchingComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (e) {
setError(e);
setRetryCount(prevCount => prevCount + 1);
} finally {
setIsLoading(false);
}
};
if (error && retryCount < 3) {
const retryDelay = Math.pow(2, retryCount) * 1000; // Exponential backoff
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
const timer = setTimeout(fetchData, retryDelay);
return () => clearTimeout(timer); // Cleanup timer on unmount or re-render
}
if (!data) {
fetchData();
}
}, [error, retryCount, data]);
if (isLoading) {
return Loading data...
;
}
if (error) {
return Error: {error.message} - Retried {retryCount} times.
;
}
return Data: {JSON.stringify(data)}
;
}
function App() {
return (
);
}
export default App;
در این مثال، DataFetchingComponent سعی در دریافت داده از یک API دارد. اگر خطایی رخ دهد، retryCount را افزایش میدهد و عملیات را پس از یک تأخیر افزایشی نمایی دوباره امتحان میکند. ErrorBoundary هرگونه استثنای مدیریت نشده را گرفته و یک پیام خطا شامل تعداد تلاشهای مجدد را نمایش میدهد.
۵. Error Boundary ها و رندرینگ سمت سرور (SSR)
هنگام استفاده از رندرینگ سمت سرور (SSR)، مدیریت خطا اهمیت بیشتری پیدا میکند. خطاهایی که در طول فرآیند رندرینگ سمت سرور رخ میدهند، میتوانند کل سرور را کرش کنند و منجر به قطعی و تجربه کاربری ضعیف شوند. شما باید اطمینان حاصل کنید که error boundary های شما به درستی برای گرفتن خطاها هم در سرور و هم در کلاینت پیکربندی شدهاند. اغلب، فریمورکهای SSR مانند Next.js و Remix مکانیزمهای مدیریت خطای داخلی خود را دارند که مکمل React Error Boundaries هستند.
۶. تست کردن Error Boundary ها
تست کردن error boundary ها برای اطمینان از عملکرد صحیح آنها و ارائه UI جایگزین مورد انتظار ضروری است. از کتابخانههای تست مانند Jest و React Testing Library برای شبیهسازی شرایط خطا و تأیید اینکه error boundary های شما خطاها را گرفته و UI جایگزین مناسب را رندر میکنند، استفاده کنید. تست انواع مختلف خطاها و موارد مرزی را برای اطمینان از قوی بودن error boundary های خود و مدیریت طیف گستردهای از سناریوها در نظر بگیرید.
مثال
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
function ComponentThatThrows() {
throw new Error('This component throws an error');
return This should not be rendered
;
}
test('renders fallback UI when an error is thrown', () => {
render(
);
const errorMessage = screen.getByText(/Something went wrong/i);
expect(errorMessage).toBeInTheDocument();
});
این تست یک کامپوننت را که خطایی را در داخل یک ErrorBoundary پرتاب میکند، رندر میکند. سپس با بررسی وجود پیام خطا در سند، تأیید میکند که UI جایگزین به درستی رندر شده است.
۷. تنزل زیبا (Graceful Degradation)
Error boundary ها یک جزء کلیدی در پیادهسازی تنزل زیبا در برنامههای ریاکت شما هستند. تنزل زیبا عمل طراحی برنامه شما به گونهای است که حتی زمانی که بخشهایی از آن از کار میافتند، با عملکرد کاهش یافته به کار خود ادامه دهد. Error boundary ها به شما امکان میدهند کامپوننتهای در حال شکست را جدا کرده و از تأثیر آنها بر بقیه برنامه جلوگیری کنید. با ارائه یک UI جایگزین و عملکرد جایگزین، میتوانید اطمینان حاصل کنید که کاربران حتی در هنگام وقوع خطاها همچنان میتوانند به ویژگیهای ضروری دسترسی داشته باشند.
اشتباهات رایج که باید از آنها اجتناب کرد
در حالی که ErrorBoundary یک ابزار قدرتمند است، برخی اشتباهات رایج وجود دارد که باید از آنها اجتناب کرد:
- نپوشاندن کدهای ناهمگام (asynchronous):
ErrorBoundaryفقط خطاهای هنگام رندرینگ، در متدهای چرخه حیات و در سازندهها را میگیرد. خطاهای موجود در کدهای ناهمگام (مانندsetTimeout،Promises) باید با استفاده از بلوکهایtry...catchگرفته شده و به طور مناسب در داخل تابع ناهمگام مدیریت شوند. - استفاده بیش از حد از Error Boundary ها: از پوشاندن بخشهای بزرگی از برنامه خود در یک
ErrorBoundaryواحد خودداری کنید. این کار میتواند جداسازی منبع خطاها را دشوار کند و منجر به نمایش بیش از حد یک UI جایگزین عمومی شود. از error boundary های جزئی برای جداسازی کامپوننتها یا ویژگیهای خاص استفاده کنید. - نادیده گرفتن اطلاعات خطا: فقط خطاها را نگیرید و یک UI جایگزین نمایش ندهید. اطمینان حاصل کنید که اطلاعات خطا (شامل پشته کامپوننت) را به یک سرویس گزارش خطا یا کنسول خود لاگ میکنید. این به شما در تشخیص و رفع مشکلات اساسی کمک میکند.
- نمایش اطلاعات حساس در پروداکشن: از نمایش اطلاعات دقیق خطا (مانند ردپای پشته) در محیطهای پروداکشن خودداری کنید. این کار میتواند اطلاعات حساس را در معرض دید کاربران قرار دهد و یک خطر امنیتی باشد. به جای آن، یک پیام خطای کاربرپسند نمایش دهید و اطلاعات دقیق را به یک سرویس گزارش خطا لاگ کنید.
Error Boundary ها با کامپوننتهای تابعی و هوکها
در حالی که Error Boundary ها به عنوان کامپوننتهای کلاسی پیادهسازی میشوند، شما همچنان میتوانید به طور مؤثر از آنها برای مدیریت خطاها در کامپوننتهای تابعی که از هوکها استفاده میکنند، بهره ببرید. رویکرد معمول شامل قرار دادن کامپوننت تابعی درون یک کامپوننت ErrorBoundary است، همانطور که قبلاً نشان داده شد. منطق مدیریت خطا در داخل ErrorBoundary قرار دارد و به طور مؤثر خطاهایی را که ممکن است در طول رندرینگ کامپوننت تابعی یا اجرای هوکها رخ دهد، جدا میکند.
به طور خاص، هر خطایی که در طول رندرینگ کامپوننت تابعی یا در بدنه یک هوک useEffect پرتاب شود، توسط ErrorBoundary گرفته خواهد شد. با این حال، مهم است توجه داشته باشید که ErrorBoundary ها خطاهایی را که در داخل کنترلکنندههای رویداد (event handlers) (مانند onClick، onChange) متصل به عناصر DOM در کامپوننت تابعی رخ میدهند، نمیگیرند. برای کنترلکنندههای رویداد، باید به استفاده از بلوکهای سنتی try...catch برای مدیریت خطا ادامه دهید.
بینالمللیسازی و محلیسازی پیامهای خطا
هنگام توسعه برنامهها برای مخاطبان جهانی، بینالمللیسازی و محلیسازی پیامهای خطای شما بسیار مهم است. پیامهای خطایی که در UI جایگزین ErrorBoundary نمایش داده میشوند، باید به زبان ترجیحی کاربر ترجمه شوند تا تجربه کاربری بهتری ارائه دهند. میتوانید از کتابخانههایی مانند i18next یا React Intl برای مدیریت ترجمههای خود و نمایش پویای پیام خطای مناسب بر اساس محلی (locale) کاربر استفاده کنید.
مثال با استفاده از i18next
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
i18next.init({
resources: {
en: {
translation: {
'error.generic': 'Something went wrong. Please try again later.',
'error.network': 'Network error. Please check your internet connection.',
},
},
fr: {
translation: {
'error.generic': 'Une erreur est survenue. Veuillez réessayer plus tard.',
'error.network': 'Erreur réseau. Veuillez vérifier votre connexion Internet.',
},
},
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
function ErrorFallback({ error }) {
const { t } = useTranslation();
let errorMessageKey = 'error.generic';
if (error instanceof NetworkError) {
errorMessageKey = 'error.network';
}
return (
{t('error.generic')}
{t(errorMessageKey)}
);
}
function ErrorBoundary({ children }) {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
static getDerivedStateFromError = (error) => {
// Update state so the next render will show the fallback UI
// return { hasError: true }; // this doesn't work with hooks as is
setHasError(true);
setError(error);
}
if (hasError) {
// You can render any custom fallback UI
return ;
}
return children;
}
export default ErrorBoundary;
در این مثال، ما از i18next برای مدیریت ترجمهها برای زبانهای انگلیسی و فرانسوی استفاده میکنیم. کامپوننت ErrorFallback از هوک useTranslation برای بازیابی پیام خطای مناسب بر اساس زبان فعلی استفاده میکند. این تضمین میکند که کاربران پیامهای خطا را به زبان ترجیحی خود مشاهده میکنند و تجربه کلی کاربر را بهبود میبخشد.
نتیجهگیری
کامپوننتهای ErrorBoundary در ریاکت ابزاری حیاتی برای ساخت برنامههای ریاکت قوی و کاربرپسند هستند. با پیادهسازی error boundary ها، میتوانید به زیبایی خطاها را مدیریت کنید، از کرش کردن برنامه جلوگیری کنید و تجربه کاربری بهتری را برای کاربران در سراسر جهان فراهم کنید. با درک اصول error boundary ها، پیادهسازی استراتژیهای پیشرفته مانند error boundary های جزئی، لاگ خطا و UI های جایگزین سفارشی، و اجتناب از اشتباهات رایج، میتوانید برنامههای ریاکت مقاومتر و قابل اعتمادتری بسازید که نیازهای مخاطبان جهانی را برآورده کند. به یاد داشته باشید که هنگام نمایش پیامهای خطا، بینالمللیسازی و محلیسازی را برای ارائه یک تجربه کاربری واقعاً فراگیر در نظر بگیرید. با افزایش پیچیدگی برنامههای وب، تسلط بر تکنیکهای مدیریت خطا برای توسعهدهندگانی که نرمافزار با کیفیت بالا میسازند، اهمیت فزایندهای خواهد یافت.