أساسيات React.js

حدود الأخطاء و Suspense

15 دقيقة الدرس 20 من 40

مقدمة إلى معالجة الأخطاء في React

يوفر React آليات قوية للتعامل مع الأخطاء بأناقة: حدود الأخطاء تلتقط أخطاء JavaScript في أشجار المكونات، بينما يدير Suspense العمليات غير المتزامنة وحالات التحميل. معًا، يخلقان تجارب مستخدم مرنة.

حدود الأخطاء: التقاط أخطاء المكونات

حدود الأخطاء هي مكونات React تلتقط أخطاء JavaScript في أي مكان في شجرة المكونات الفرعية، وتسجل الأخطاء، وتعرض واجهة مستخدم احتياطية. تعمل مثل كتل try/catch في JavaScript ولكن للمكونات.

مهم: يجب أن تكون حدود الأخطاء مكونات فئة. لا يمكن أن تكون المكونات الوظيفية حدود أخطاء (بعد)، لكن يمكن تغليفها بها.

// src/components/ErrorBoundary.jsx
import React from 'react';

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, errorInfo) {
    // تسجيل الخطأ في خدمة الإبلاغ عن الأخطاء
    console.error('خطأ تم التقاطه بواسطة الحد:', error, errorInfo);

    // يمكنك التسجيل في خدمات مثل Sentry، LogRocket، إلخ.
    // logErrorToService(error, errorInfo);

    this.setState({
      error,
      errorInfo
    });
  }

  resetError = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null
    });
  };

  render() {
    if (this.state.hasError) {
      // واجهة المستخدم الاحتياطية
      return (
        <div style={{
          padding: '2rem',
          backgroundColor: '#fee',
          border: '1px solid #fcc',
          borderRadius: '0.5rem'
        }}>
          <h2>حدث خطأ ما</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <summary>تفاصيل الخطأ</summary>
            <p>{this.state.error && this.state.error.toString()}</p>
            <p>{this.state.errorInfo && this.state.errorInfo.componentStack}</p>
          </details>
          <button onClick={this.resetError}>حاول مرة أخرى</button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

استخدام حدود الأخطاء

قم بتغليف المكونات التي قد تطرح أخطاء بحد الأخطاء:

import ErrorBoundary from './components/ErrorBoundary';
import UserProfile from './components/UserProfile';
import Dashboard from './components/Dashboard';

function App() {
  return (
    <div>
      <h1>تطبيقي</h1>

      {/* تغليف المكونات الفردية */}
      <ErrorBoundary>
        <UserProfile userId={123} />
      </ErrorBoundary>

      {/* أو تغليف أقسام كاملة */}
      <ErrorBoundary>
        <Dashboard>
          <Stats />
          <Charts />
          <RecentActivity />
        </Dashboard>
      </ErrorBoundary>
    </div>
  );
}

// مكون قد يطرح خطأ
function BuggyComponent({ user }) {
  if (!user) {
    throw new Error('المستخدم مطلوب!');
  }

  return <div>{user.name}</div>;
}

حدود الأخطاء لا تلتقط:

  • الأخطاء في معالجات الأحداث (استخدم try/catch)
  • الكود غير المتزامن (setTimeout، الوعود)
  • أخطاء العرض من جانب الخادم
  • الأخطاء المطروحة في حد الأخطاء نفسه

حد أخطاء متقدم مع التسجيل

أنشئ حد أخطاء جاهز للإنتاج مع تتبع الأخطاء:

import React from 'react';

class ProductionErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
      eventId: null
    };
  }

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

  componentDidCatch(error, errorInfo) {
    // التسجيل في خدمة خارجية في الإنتاج
    if (process.env.NODE_ENV === 'production') {
      // مثال: تكامل Sentry
      // const eventId = Sentry.captureException(error, {
      //   contexts: { react: { componentStack: errorInfo.componentStack } }
      // });
      // this.setState({ eventId });

      // أو خدمة التسجيل المخصصة الخاصة بك
      fetch('/api/log-error', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          error: error.toString(),
          componentStack: errorInfo.componentStack,
          timestamp: new Date().toISOString(),
          userAgent: navigator.userAgent,
          url: window.location.href
        })
      });
    }

    this.setState({ error, errorInfo });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>عفوًا! حدث خطأ ما</h2>
          <p>تم إخطارنا ونحن نعمل على حل المشكلة.</p>

          {process.env.NODE_ENV === 'development' && (
            <details>
              <summary>تفاصيل الخطأ (التطوير فقط)</summary>
              <pre>{this.state.error.toString()}</pre>
              <pre>{this.state.errorInfo.componentStack}</pre>
            </details>
          )}

          <button onClick={() => window.location.reload()}>
            إعادة تحميل الصفحة
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Suspense: إدارة العمليات غير المتزامنة

يتيح Suspense للمكونات "الانتظار" لشيء ما قبل العرض، مما يظهر واجهة مستخدم احتياطية أثناء التحميل:

import { Suspense, lazy } from 'react';

// تحميل المكونات كسولًا
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));
const Dashboard = lazy(() => import('./components/Dashboard'));
const UserProfile = lazy(() => import('./components/UserProfile'));

function App() {
  return (
    <div>
      {/* Suspense أساسي مع احتياطي تحميل */}
      <Suspense fallback={<div>جاري التحميل...</div>}>
        <HeavyComponent />
      </Suspense>

      {/* مكونات متعددة في Suspense واحد */}
      <Suspense fallback={<LoadingSpinner />}>
        <Dashboard />
        <UserProfile userId={123} />
      </Suspense>

      {/* حدود Suspense المتداخلة */}
      <Suspense fallback={<div>جاري تحميل الصفحة...</div>}>
        <MainLayout>
          <Suspense fallback={<div>جاري تحميل الشريط الجانبي...</div>}>
            <Sidebar />
          </Suspense>
          <Suspense fallback={<div>جاري تحميل المحتوى...</div>}>
            <Content />
          </Suspense>
        </MainLayout>
      </Suspense>
    </div>
  );
}

// مكون تحميل مخصص
function LoadingSpinner() {
  return (
    <div style={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      height: '200px'
    }}>
      <div className="spinner">جاري التحميل...</div>
    </div>
  );
}

دمج حدود الأخطاء و Suspense

استخدم كليهما معًا لمعالجة أخطاء قوية وحالات تحميل:

import { Suspense, lazy } from 'react';
import ErrorBoundary from './components/ErrorBoundary';

const AsyncComponent = lazy(() => import('./components/AsyncComponent'));

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<LoadingFallback />}>
        <AsyncComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// أفضل: فصل حالات التحميل والأخطاء
function RobustAsyncLoader({ children }) {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<LoadingFallback />}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// الاستخدام
function Dashboard() {
  return (
    <div>
      <h1>لوحة التحكم</h1>

      <RobustAsyncLoader>
        <AsyncCharts />
      </RobustAsyncLoader>

      <RobustAsyncLoader>
        <AsyncUserList />
      </RobustAsyncLoader>
    </div>
  );
}

واجهة مستخدم تحميل هيكلية

أنشئ تجارب تحميل أفضل بشاشات هيكلية:

// مكون هيكلي لحالة التحميل
function UserCardSkeleton() {
  return (
    <div className="user-card skeleton">
      <div className="skeleton-avatar"></div>
      <div className="skeleton-text"></div>
      <div className="skeleton-text short"></div>
    </div>
  );
}

/* CSS الهيكلي */
.skeleton {
  animation: pulse 1.5s ease-in-out infinite;
}

.skeleton-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #e0e0e0;
}

.skeleton-text {
  height: 16px;
  background: #e0e0e0;
  border-radius: 4px;
  margin: 8px 0;
}

.skeleton-text.short {
  width: 60%;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

// الاستخدام مع Suspense
function UserList() {
  return (
    <Suspense fallback={
      <div>
        <UserCardSkeleton />
        <UserCardSkeleton />
        <UserCardSkeleton />
      </div>
    }>
      <AsyncUserList />
    </Suspense>
  );
}

استراتيجيات استعادة الأخطاء

نفّذ استراتيجيات استعادة مختلفة بناءً على أنواع الأخطاء:

import React from 'react';

class SmartErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      retryCount: 0
    };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('خطأ:', error, errorInfo);
  }

  handleRetry = () => {
    this.setState(prevState => ({
      hasError: false,
      error: null,
      retryCount: prevState.retryCount + 1
    }));
  };

  handleReload = () => {
    window.location.reload();
  };

  handleGoHome = () => {
    window.location.href = '/';
  };

  render() {
    if (this.state.hasError) {
      const { error, retryCount } = this.state;
      const isNetworkError = error.message.includes('network') ||
                             error.message.includes('fetch');

      return (
        <div className="error-recovery">
          <h2>حدث خطأ ما</h2>

          {isNetworkError && (
            <>
              <p>تم اكتشاف مشكلة في اتصال الشبكة.</p>
              <button onClick={this.handleRetry}>
                إعادة المحاولة ({3 - retryCount} محاولات متبقية)
              </button>
            </>
          )}

          {!isNetworkError && (
            <>
              <p>حدث خطأ غير متوقع.</p>
              <button onClick={this.handleReload}>إعادة تحميل الصفحة</button>
              <button onClick={this.handleGoHome}>العودة إلى الرئيسية</button>
            </>
          )}

          {retryCount >= 3 && (
            <p>
              إذا استمرت المشكلة، يرجى الاتصال بالدعم.
            </p>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

أفضل ممارسة: ضع حدود الأخطاء بشكل استراتيجي على مستويات مختلفة—واحد في جذر التطبيق للأخطاء الكارثية، وآخرين حول الميزات للتعامل المعزول مع الأخطاء.

الميزات المتزامنة مع Suspense

React 18+ Suspense يعمل مع الميزات المتزامنة لتجربة مستخدم أفضل:

import { Suspense, useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    const value = e.target.value;

    // وضع علامة على تحديث الحالة كغير عاجل
    startTransition(() => {
      setQuery(value);
    });
  };

  return (
    <div>
      <input
        type="text"
        placeholder="بحث..."
        onChange={handleSearch}
        style={{ opacity: isPending ? 0.5 : 1 }}
      />

      <Suspense fallback={<div>جاري البحث...</div>}>
        <ResultsList query={query} />
      </Suspense>
    </div>
  );
}

تمرين 1: نظام حدود الأخطاء

المهمة: بناء نظام شامل لمعالجة الأخطاء:

  • أنشئ مكون حد أخطاء بواجهة مستخدم احتياطية مخصصة
  • أضف تسجيل الأخطاء إلى وحدة التحكم (محاكاة خدمة خارجية)
  • نفّذ وظيفة إعادة المحاولة
  • أنشئ مكونًا معطوبًا يطرح أخطاء عند النقر على زر
  • أظهر رسائل خطأ مختلفة لأنواع أخطاء مختلفة

تمرين 2: لوحة تحكم بالتحميل الكسول

المهمة: بناء لوحة تحكم بأقسام محملة كسولًا:

  • أنشئ مكونات منفصلة للرسوم البيانية والإحصائيات وأقسام النشاط
  • حمّل كل قسم كسولًا بـ React.lazy()
  • أضف حدود Suspense بواجهة مستخدم تحميل هيكلية
  • اغلف كل قسم في حدود الأخطاء
  • أضف زر "إعادة تحميل القسم" في احتياطي الخطأ

تمرين 3: محمل بيانات غير متزامن قوي

المهمة: أنشئ مكون محمل بيانات غير متزامن قابل لإعادة الاستخدام:

  • ادمج حد الأخطاء و Suspense
  • اقبل خصائص العرض لحالات التحميل والخطأ والنجاح
  • نفّذ إعادة محاولة تلقائية بتراجع أسي
  • أضف معالجة المهلة
  • اعرض عدد إعادة المحاولة والوقت المقدر لإعادة المحاولة التالية

الخلاصة

في هذا الدرس، أتقنت معالجة الأخطاء وإدارة العمليات غير المتزامنة في React:

  • إنشاء مكونات حد الأخطاء للتقاط الأخطاء والتعامل معها بأناقة
  • فهم ما يمكن وما لا يمكن لحدود الأخطاء التقاطه
  • تطبيق تسجيل وتتبع الأخطاء الجاهز للإنتاج
  • استخدام Suspense للتحميل الكسول والعمليات غير المتزامنة
  • دمج حدود الأخطاء و Suspense للتطبيقات القوية
  • بناء واجهات مستخدم تحميل هيكلية لتجربة مستخدم أفضل
  • تنفيذ استراتيجيات استعادة الأخطاء الذكية
  • الاستفادة من الميزات المتزامنة للأداء الأمثل

مع إتقان أساسيات React.js هذه، أنت جاهز لبناء تطبيقات بجودة الإنتاج بمعالجة أخطاء مناسبة وأداء محسّن وتجربة مستخدم ممتازة!