تعلم كيفية استخدام حدود الأخطاء (ErrorBoundaries) في React لمعالجة الأخطاء بأناقة، ومنع تعطل التطبيق، وتوفير تجربة مستخدم أفضل مع استراتيجيات تعافي قوية.
مكون ErrorBoundary في React: استراتيجيات عزل الأخطاء والتعافي منها
في عالم تطوير الواجهات الأمامية الديناميكي، خاصة عند العمل مع أطر عمل معقدة قائمة على المكونات مثل React، فإن الأخطاء غير المتوقعة أمر لا مفر منه. هذه الأخطاء، إذا لم يتم التعامل معها بشكل صحيح، يمكن أن تؤدي إلى تعطل التطبيق وتجربة مستخدم محبطة. يقدم مكون ErrorBoundary في React حلاً قويًا للتعامل مع هذه الأخطاء بأناقة وعزلها وتوفير استراتيجيات للتعافي منها. يستكشف هذا الدليل الشامل قوة ErrorBoundary، ويوضح كيفية تنفيذه بفعالية لبناء تطبيقات React أكثر مرونة وسهولة في الاستخدام لجمهور عالمي.
فهم الحاجة إلى حدود الأخطاء
قبل الخوض في التنفيذ، دعونا نفهم سبب أهمية حدود الأخطاء. في React، يمكن للأخطاء التي تحدث أثناء العرض (rendering)، أو في دوال دورة حياة المكون (lifecycle methods)، أو في مُنشِئات المكونات الفرعية (constructors of child components) أن تتسبب في تعطل التطبيق بأكمله. وذلك لأن الأخطاء التي لم يتم التقاطها تنتشر لأعلى في شجرة المكونات، مما يؤدي غالبًا إلى ظهور شاشة بيضاء أو رسالة خطأ غير مفيدة. تخيل مستخدمًا في اليابان يحاول إكمال معاملة مالية مهمة، ليواجه شاشة فارغة بسبب خطأ بسيط في مكون لا يبدو ذا صلة. يوضح هذا الحاجة الماسة إلى إدارة استباقية للأخطاء.
توفر حدود الأخطاء طريقة لالتقاط أخطاء جافاسكريبت في أي مكان في شجرة المكونات الفرعية الخاصة بها، وتسجيل تلك الأخطاء، وعرض واجهة مستخدم بديلة بدلاً من تعطل شجرة المكونات. إنها تسمح لك بعزل المكونات المعيبة ومنع الأخطاء في جزء واحد من تطبيقك من التأثير على الأجزاء الأخرى، مما يضمن تجربة مستخدم أكثر استقرارًا وموثوقية على مستوى العالم.
ما هو مكون ErrorBoundary في React؟
مكون ErrorBoundary هو مكون React يلتقط أخطاء جافاسكريبت في أي مكان في شجرة المكونات الفرعية الخاصة به، ويسجل تلك الأخطاء، ويعرض واجهة مستخدم بديلة. وهو مكون من نوع class component ينفذ إحدى دالتي دورة الحياة التاليتين أو كلتيهما:
static getDerivedStateFromError(error): تُستدعى دالة دورة الحياة هذه بعد حدوث خطأ من مكون فرعي. تتلقى الخطأ الذي حدث كوسيط ويجب أن تعيد قيمة لتحديث حالة المكون.componentDidCatch(error, info): تُستدعى دالة دورة الحياة هذه بعد حدوث خطأ من مكون فرعي. تتلقى وسيطين: الخطأ الذي حدث وكائن معلومات (info object) يحتوي على معلومات حول المكون الذي أدى إلى الخطأ. يمكنك استخدام هذه الدالة لتسجيل معلومات الخطأ أو تنفيذ تأثيرات جانبية أخرى.
إنشاء مكون ErrorBoundary أساسي
دعنا ننشئ مكون ErrorBoundary أساسي لتوضيح المبادئ الأساسية.
مثال الكود
إليك كود لمكون ErrorBoundary بسيط:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
// تحديث الحالة حتى يُظهر العرض التالي واجهة المستخدم البديلة.
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) {
// يمكنك عرض أي واجهة مستخدم بديلة مخصصة
return (
حدث خطأ ما.
الخطأ: {this.state.error && this.state.error.toString()}
{this.state.errorInfo && this.state.errorInfo.componentStack}
);
}
return this.props.children;
}
}
export default ErrorBoundary;
الشرح
- المُنشِئ (Constructor): يقوم المُنشِئ بتهيئة حالة المكون الأولية حيث تكون قيمة
hasErrorهيfalse. نقوم أيضًا بتخزين الخطأ ومعلومات الخطأ لأغراض تصحيح الأخطاء. getDerivedStateFromError(error): تُستدعى هذه الدالة الثابتة (static method) عند حدوث خطأ من مكون فرعي. تقوم بتحديث الحالة للإشارة إلى حدوث خطأ.componentDidCatch(error, info): تُستدعى هذه الدالة بعد حدوث الخطأ. تتلقى الخطأ وكائنinfoيحتوي على معلومات حول مكدس المكونات (component stack). هنا، نقوم بتسجيل الخطأ في الكونسول (استبدل هذا بآلية التسجيل المفضلة لديك، مثل Sentry أو Bugsnag أو حل داخلي مخصص). نقوم أيضًا بتعيين الخطأ ومعلومات الخطأ في الحالة.render(): تتحقق دالة العرض (render) من حالةhasError. إذا كانتtrue، فإنها تعرض واجهة مستخدم بديلة؛ وإلا، فإنها تعرض المكونات الأبناء. يجب أن تكون واجهة المستخدم البديلة مفيدة وسهلة الاستخدام. يجب عرض تفاصيل الخطأ ومكدس المكونات بشكل شرطي أو إزالتها في بيئات الإنتاج لأسباب أمنية، على الرغم من كونها مفيدة للمطورين.
استخدام مكون ErrorBoundary
لاستخدام مكون ErrorBoundary، ما عليك سوى تغليف أي مكون قد يطلق خطأ بداخله.
مثال الكود
import ErrorBoundary from './ErrorBoundary';
function MyComponent() {
return (
{/* مكونات قد تطلق خطأ */}
);
}
function App() {
return (
);
}
export default App;
الشرح
في هذا المثال، يتم تغليف MyComponent بـ ErrorBoundary. إذا حدث أي خطأ داخل MyComponent أو مكوناته الفرعية، فسوف يلتقطه ErrorBoundary ويعرض واجهة المستخدم البديلة.
استراتيجيات متقدمة لـ ErrorBoundary
بينما يوفر ErrorBoundary الأساسي مستوى أساسيًا من معالجة الأخطاء، هناك العديد من الاستراتيجيات المتقدمة التي يمكنك تنفيذها لتعزيز إدارة الأخطاء لديك.
1. حدود الأخطاء الجزئية (Granular)
بدلاً من تغليف التطبيق بأكمله بـ ErrorBoundary واحد، فكر في استخدام حدود أخطاء جزئية. يتضمن ذلك وضع مكونات ErrorBoundary حول أجزاء معينة من تطبيقك تكون أكثر عرضة للأخطاء أو حيث يكون للفشل تأثير محدود. على سبيل المثال، يمكنك تغليف عناصر واجهة مستخدم فردية (widgets) أو مكونات تعتمد على مصادر بيانات خارجية.
مثال
function ProductList() {
return (
{/* قائمة المنتجات */}
);
}
function RecommendationWidget() {
return (
{/* محرك التوصيات */}
);
}
function App() {
return (
);
}
في هذا المثال، لدى RecommendationWidget مكون ErrorBoundary خاص به. إذا فشل محرك التوصيات، فلن يؤثر ذلك على ProductList، وسيظل بإمكان المستخدم تصفح المنتجات. يعمل هذا النهج الجزئي على تحسين تجربة المستخدم الإجمالية عن طريق عزل الأخطاء ومنعها من الانتشار عبر التطبيق.
2. تسجيل الأخطاء والإبلاغ عنها
يعد تسجيل الأخطاء أمرًا بالغ الأهمية لتصحيح الأخطاء وتحديد المشكلات المتكررة. تعتبر دالة دورة الحياة 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) {
// تحديث الحالة حتى يُظهر العرض التالي واجهة المستخدم البديلة.
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// تسجيل الخطأ إلى Sentry
Sentry.captureException(error, { extra: info });
// يمكنك أيضًا تسجيل الخطأ في خدمة الإبلاغ عن الأخطاء
console.error("Caught an error:", error);
}
render() {
if (this.state.hasError) {
// يمكنك عرض أي واجهة مستخدم بديلة مخصصة
return (
حدث خطأ ما.
);
}
return this.props.children;
}
}
export default ErrorBoundary;
في هذا المثال، تستخدم دالة componentDidCatch التابع Sentry.captureException للإبلاغ عن الخطأ إلى Sentry. يمكنك تكوين Sentry لإرسال إشعارات إلى فريقك، مما يتيح لك الاستجابة بسرعة للأخطاء الحرجة.
3. واجهة مستخدم بديلة مخصصة
تعتبر واجهة المستخدم البديلة التي يعرضها ErrorBoundary فرصة لتوفير تجربة سهلة الاستخدام حتى عند حدوث أخطاء. بدلاً من عرض رسالة خطأ عامة، فكر في عرض رسالة أكثر إفادة توجه المستخدم نحو حل. قد يشمل ذلك تعليمات حول كيفية تحديث الصفحة، أو الاتصال بالدعم، أو المحاولة مرة أخرى لاحقًا. يمكنك أيضًا تخصيص واجهة المستخدم البديلة بناءً على نوع الخطأ الذي حدث.
مثال
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error) {
// تحديث الحالة حتى يُظهر العرض التالي واجهة المستخدم البديلة.
return {
hasError: true,
error: error,
};
}
componentDidCatch(error, info) {
console.error("Caught an error:", error);
// يمكنك أيضًا تسجيل الخطأ في خدمة الإبلاغ عن الأخطاء
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// يمكنك عرض أي واجهة مستخدم بديلة مخصصة
if (this.state.error instanceof NetworkError) {
return (
خطأ في الشبكة
يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.
);
} else {
return (
حدث خطأ ما.
يرجى محاولة تحديث الصفحة أو الاتصال بالدعم.
);
}
}
return this.props.children;
}
}
export default ErrorBoundary;
في هذا المثال، تتحقق واجهة المستخدم البديلة مما إذا كان الخطأ هو NetworkError. إذا كان كذلك، فإنها تعرض رسالة محددة تطلب من المستخدم التحقق من اتصاله بالإنترنت. وإلا، فإنها تعرض رسالة خطأ عامة. يمكن أن يؤدي توفير إرشادات محددة وقابلة للتنفيذ إلى تحسين تجربة المستخدم بشكل كبير.
4. آليات إعادة المحاولة
في بعض الحالات، تكون الأخطاء عابرة ويمكن حلها عن طريق إعادة محاولة العملية. يمكنك تنفيذ آلية إعادة محاولة داخل 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; // تراجع أسي
console.log(`إعادة المحاولة خلال ${retryDelay / 1000} ثانية...`);
const timer = setTimeout(fetchData, retryDelay);
return () => clearTimeout(timer); // تنظيف المؤقت عند إلغاء تحميل المكون أو إعادة عرضه
}
if (!data) {
fetchData();
}
}, [error, retryCount, data]);
if (isLoading) {
return جاري تحميل البيانات...
;
}
if (error) {
return خطأ: {error.message} - تمت إعادة المحاولة {retryCount} مرات.
;
}
return البيانات: {JSON.stringify(data)}
;
}
function App() {
return (
);
}
export default App;
في هذا المثال، يحاول DataFetchingComponent جلب البيانات من واجهة برمجة تطبيقات (API). إذا حدث خطأ، فإنه يزيد من retryCount ويعيد محاولة العملية بعد تأخير يزداد بشكل أسي. يلتقط ErrorBoundary أي استثناءات غير معالجة ويعرض رسالة خطأ، بما في ذلك عدد محاولات إعادة المحاولة.
5. حدود الأخطاء والعرض من جانب الخادم (SSR)
عند استخدام العرض من جانب الخادم (SSR)، تصبح معالجة الأخطاء أكثر أهمية. يمكن للأخطاء التي تحدث أثناء عملية العرض من جانب الخادم أن تعطل الخادم بأكمله، مما يؤدي إلى توقف الخدمة وتجربة مستخدم سيئة. تحتاج إلى التأكد من أن حدود الأخطاء لديك مهيأة بشكل صحيح لالتقاط الأخطاء على كل من الخادم والعميل. غالبًا ما تحتوي أطر عمل SSR مثل Next.js و Remix على آليات مدمجة خاصة بها لمعالجة الأخطاء تكمل حدود الأخطاء في React.
6. اختبار حدود الأخطاء
يعد اختبار حدود الأخطاء أمرًا ضروريًا لضمان عملها بشكل صحيح وتوفير واجهة المستخدم البديلة المتوقعة. استخدم مكتبات الاختبار مثل Jest و React Testing Library لمحاكاة ظروف الخطأ والتحقق من أن حدود الأخطاء تلتقط الأخطاء وتعرض واجهة المستخدم البديلة المناسبة. ضع في اعتبارك اختبار أنواع مختلفة من الأخطاء والحالات القصوى لضمان قوة حدود الأخطاء لديك وقدرتها على التعامل مع مجموعة واسعة من السيناريوهات.
مثال
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
function ComponentThatThrows() {
throw new Error('هذا المكون يطلق خطأ');
return لا يجب عرض هذا
;
}
test('يعرض واجهة المستخدم البديلة عند إطلاق خطأ', () => {
render(
);
const errorMessage = screen.getByText(/حدث خطأ ما/i);
expect(errorMessage).toBeInTheDocument();
});
يعرض هذا الاختبار مكونًا يطلق خطأ داخل ErrorBoundary. ثم يتحقق من عرض واجهة المستخدم البديلة بشكل صحيح عن طريق التحقق من وجود رسالة الخطأ في المستند.
7. التدهور التدريجي (Graceful Degradation)
تعتبر حدود الأخطاء مكونًا رئيسيًا في تنفيذ التدهور التدريجي في تطبيقات React الخاصة بك. التدهور التدريجي هو ممارسة تصميم تطبيقك للاستمرار في العمل، وإن كان بوظائف منخفضة، حتى عندما تفشل أجزاء منه. تسمح لك حدود الأخطاء بعزل المكونات الفاشلة ومنعها من التأثير على بقية التطبيق. من خلال توفير واجهة مستخدم بديلة ووظائف بديلة، يمكنك ضمان أن يظل بإمكان المستخدمين الوصول إلى الميزات الأساسية حتى عند حدوث أخطاء.
الأخطاء الشائعة التي يجب تجنبها
بينما يعد ErrorBoundary أداة قوية، هناك بعض الأخطاء الشائعة التي يجب تجنبها:
- عدم تغليف الكود غير المتزامن: يلتقط
ErrorBoundaryالأخطاء فقط أثناء العرض، وفي دوال دورة الحياة، وفي المُنشِئات. يجب التقاط الأخطاء في الكود غير المتزامن (مثلsetTimeout،Promises) باستخدام كتلtry...catchومعالجتها بشكل مناسب داخل الدالة غير المتزامنة. - الإفراط في استخدام حدود الأخطاء: تجنب تغليف أجزاء كبيرة من تطبيقك في
ErrorBoundaryواحد. قد يجعل هذا من الصعب عزل مصدر الأخطاء ويمكن أن يؤدي إلى عرض واجهة مستخدم بديلة عامة في كثير من الأحيان. استخدم حدود أخطاء جزئية لعزل مكونات أو ميزات معينة. - تجاهل معلومات الخطأ: لا تكتفِ بالتقاط الأخطاء وعرض واجهة مستخدم بديلة. تأكد من تسجيل معلومات الخطأ (بما في ذلك مكدس المكونات) في خدمة الإبلاغ عن الأخطاء أو في الكونسول. سيساعدك هذا على تشخيص المشكلات الأساسية وإصلاحها.
- عرض معلومات حساسة في بيئة الإنتاج: تجنب عرض معلومات خطأ مفصلة (مثل تتبعات المكدس) في بيئات الإنتاج. يمكن أن يكشف هذا عن معلومات حساسة للمستخدمين ويمكن أن يمثل خطرًا أمنيًا. بدلاً من ذلك، اعرض رسالة خطأ سهلة الاستخدام وسجل المعلومات التفصيلية في خدمة الإبلاغ عن الأخطاء.
حدود الأخطاء مع المكونات الوظيفية والخطافات (Hooks)
بينما يتم تنفيذ حدود الأخطاء كمكونات من نوع class، لا يزال بإمكانك استخدامها بفعالية لمعالجة الأخطاء داخل المكونات الوظيفية التي تستخدم الخطافات (hooks). يتضمن النهج المعتاد تغليف المكون الوظيفي داخل مكون ErrorBoundary، كما هو موضح سابقًا. يكمن منطق معالجة الأخطاء داخل ErrorBoundary، مما يعزل بشكل فعال الأخطاء التي قد تحدث أثناء عرض المكون الوظيفي أو تنفيذ الخطافات.
على وجه التحديد، سيتم التقاط أي أخطاء يتم إطلاقها أثناء عرض المكون الوظيفي أو داخل متن خطاف useEffect بواسطة ErrorBoundary. ومع ذلك، من المهم ملاحظة أن حدود الأخطاء لا تلتقط الأخطاء التي تحدث داخل معالجات الأحداث (مثل onClick، onChange) المرتبطة بعناصر DOM داخل المكون الوظيفي. بالنسبة لمعالجات الأحداث، يجب أن تستمر في استخدام كتل try...catch التقليدية لمعالجة الأخطاء.
تدويل وترجمة رسائل الأخطاء
عند تطوير تطبيقات لجمهور عالمي، من الضروري تدويل وترجمة رسائل الأخطاء الخاصة بك. يجب ترجمة رسائل الخطأ المعروضة في واجهة المستخدم البديلة لـ ErrorBoundary إلى اللغة المفضلة للمستخدم لتوفير تجربة مستخدم أفضل. يمكنك استخدام مكتبات مثل i18next أو React Intl لإدارة ترجماتك وعرض رسالة الخطأ المناسبة ديناميكيًا بناءً على لغة المستخدم.
مثال باستخدام 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.',
},
},
ar: {
translation: {
'error.generic': 'حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقًا.',
'error.network': 'خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت.',
},
},
},
lng: 'ar',
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) => {
// تحديث الحالة حتى يُظهر العرض التالي واجهة المستخدم البديلة
// return { hasError: true }; // هذا لا يعمل مع الخطافات (hooks) كما هو
setHasError(true);
setError(error);
}
if (hasError) {
// يمكنك عرض أي واجهة مستخدم بديلة مخصصة
return ;
}
return children;
}
export default ErrorBoundary;
في هذا المثال، نستخدم i18next لإدارة الترجمات للغة الإنجليزية والعربية. يستخدم مكون ErrorFallback خطاف useTranslation لاسترداد رسالة الخطأ المناسبة بناءً على اللغة الحالية. هذا يضمن أن يرى المستخدمون رسائل الخطأ بلغتهم المفضلة، مما يعزز تجربة المستخدم الإجمالية.
الخاتمة
تعتبر مكونات ErrorBoundary في React أداة حاسمة لبناء تطبيقات React قوية وسهلة الاستخدام. من خلال تنفيذ حدود الأخطاء، يمكنك التعامل مع الأخطاء بأناقة، ومنع تعطل التطبيق، وتوفير تجربة مستخدم أفضل للمستخدمين في جميع أنحاء العالم. من خلال فهم مبادئ حدود الأخطاء، وتنفيذ استراتيجيات متقدمة مثل حدود الأخطاء الجزئية، وتسجيل الأخطاء، وواجهات المستخدم البديلة المخصصة، وتجنب الأخطاء الشائعة، يمكنك بناء تطبيقات React أكثر مرونة وموثوقية تلبي احتياجات جمهور عالمي. تذكر أن تأخذ في الاعتبار التدويل والترجمة عند عرض رسائل الخطأ لتوفير تجربة مستخدم شاملة حقًا. مع استمرار نمو تعقيد تطبيقات الويب، ستصبح إتقان تقنيات معالجة الأخطاء ذا أهمية متزايدة للمطورين الذين يبنون برامج عالية الجودة.