أساسيات React.js

دورة حياة المكون والأداء

18 دقيقة الدرس 19 من 40

مقدمة إلى دورة حياة المكون

فهم دورة حياة مكون React وتقنيات التحسين أمر بالغ الأهمية لبناء تطبيقات عالية الأداء. يغطي هذا الدرس مفاهيم دورة الحياة في React الحديث، وتحسين الأداء باستخدام React.memo و useMemo و useCallback وأدوات التحليل.

دورة حياة المكون في المكونات الوظيفية

تستخدم المكونات الوظيفية الخطافات للتعامل مع أحداث دورة الحياة. يحل خطاف useEffect محل طرق دورة حياة مكونات الفئة:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // ComponentDidMount + ComponentDidUpdate (عند تغيير userId)
  useEffect(() => {
    console.log('تم تركيب المكون أو تغيير userId');

    setLoading(true);
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });

    // ComponentWillUnmount (وظيفة التنظيف)
    return () => {
      console.log('التنظيف قبل إعادة تشغيل التأثير أو إلغاء التركيب');
    };
  }, [userId]); // مصفوفة التبعية - يعمل التأثير عند تغيير userId

  // يعمل فقط عند التركيب (مصفوفة تبعية فارغة)
  useEffect(() => {
    console.log('تم تركيب المكون مرة واحدة');
    document.title = `ملف المستخدم - ${userId}`;

    return () => {
      console.log('إلغاء تركيب المكون');
      document.title = 'تطبيق React';
    };
  }, []); // مصفوفة فارغة = التركيب/إلغاء التركيب فقط

  // يعمل في كل عرض (بدون مصفوفة تبعية - تجنب هذا!)
  useEffect(() => {
    console.log('يعمل بعد كل عرض');
  });

  if (loading) return <div>جاري التحميل...</div>;
  if (!user) return <div>المستخدم غير موجود</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

معادلات دورة الحياة:

  • useEffect(() => {}, []) = componentDidMount
  • useEffect(() => {}) = componentDidMount + componentDidUpdate
  • return () => {} في useEffect = componentWillUnmount

React.memo: منع إعادة العرض غير الضرورية

React.memo هو مكون ذو ترتيب أعلى يحفظ إخراج المكون، مما يمنع إعادة العرض عندما لا تتغير الخصائص:

import { memo } from 'react';

// بدون memo - يعيد العرض في كل مرة يعيد الأصل العرض
function ExpensiveComponent({ data, count }) {
  console.log('تم عرض ExpensiveComponent');

  // تخيل حسابات مكلفة هنا
  const processedData = data.map(item => ({
    ...item,
    calculated: item.value * 1000
  }));

  return (
    <div>
      <h3>العدد: {count}</h3>
      <ul>
        {processedData.map(item => (
          <li key={item.id}>{item.calculated}</li>
        ))}
      </ul>
    </div>
  );
}

// مع memo - يعيد العرض فقط عند تغيير الخصائص
const MemoizedExpensiveComponent = memo(ExpensiveComponent);

// الاستخدام
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  const data = [
    { id: 1, value: 10 },
    { id: 2, value: 20 },
    { id: 3, value: 30 }
  ];

  return (
    <div>
      {/* سيعيد العرض فقط عند تغيير count أو data */}
      <MemoizedExpensiveComponent data={data} count={count} />

      <button onClick={() => setCount(c => c + 1)}>
        زيادة العدد
      </button>

      {/* لن يؤدي هذا إلى إعادة عرض MemoizedExpensiveComponent */}
      <button onClick={() => setOtherState(s => s + 1)}>
        تحديث حالة أخرى ({otherState})
      </button>
    </div>
  );
}

متى تستخدم React.memo: استخدمه للمكونات التي تُعرض غالبًا بنفس الخصائص، خاصة إذا كانت تؤدي حسابات مكلفة أو تعرض قوائم كبيرة.

وظيفة المقارنة المخصصة مع React.memo

يمكنك توفير وظيفة مقارنة مخصصة لمقارنات الخصائص المعقدة:

import { memo } from 'react';

function UserCard({ user, onEdit }) {
  console.log('تم عرض UserCard');

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>تحرير</button>
    </div>
  );
}

// مقارنة مخصصة: إعادة العرض فقط إذا تغير user.id أو user.name
const MemoizedUserCard = memo(UserCard, (prevProps, nextProps) => {
  // إرجاع true إذا كانت الخصائص متساوية (تخطي العرض)
  // إرجاع false إذا كانت الخصائص مختلفة (إعادة العرض)
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name
    // ملاحظة: تغييرات مرجع وظيفة onEdit لن تؤدي إلى إعادة العرض
  );
});

export default MemoizedUserCard;

تحذير: React.memo يستخدم المقارنة الضحلة بشكل افتراضي. للكائنات والمصفوفات، قدّم وظيفة مقارنة مخصصة أو تأكد من استقرار المرجع.

useMemo: حفظ الحسابات المكلفة

useMemo يخزن مؤقتًا نتيجة الحسابات المكلفة بين العروض:

import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [sortOrder, setSortOrder] = useState('asc');

  // بدون useMemo - يُعاد حسابها في كل عرض
  const filteredProducts = products
    .filter(p => p.category === filter)
    .sort((a, b) => {
      return sortOrder === 'asc'
        ? a.price - b.price
        : b.price - a.price;
    });

  // مع useMemo - يُعاد حسابها فقط عند تغيير التبعيات
  const memoizedFilteredProducts = useMemo(() => {
    console.log('إعادة حساب المنتجات المفلترة');

    return products
      .filter(p => p.category === filter)
      .sort((a, b) => {
        return sortOrder === 'asc'
          ? a.price - b.price
          : b.price - a.price;
      });
  }, [products, filter, sortOrder]); // التبعيات

  return (
    <div>
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        الفرز: {sortOrder}
      </button>

      <ul>
        {memoizedFilteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

// المزيد من أمثلة useMemo
function AnalyticsDashboard({ data }) {
  // حسابات مكلفة
  const statistics = useMemo(() => {
    console.log('حساب الإحصائيات...');

    const total = data.reduce((sum, item) => sum + item.value, 0);
    const average = total / data.length;
    const max = Math.max(...data.map(d => d.value));
    const min = Math.min(...data.map(d => d.value));

    return { total, average, max, min };
  }, [data]);

  return (
    <div>
      <p>الإجمالي: {statistics.total}</p>
      <p>المتوسط: {statistics.average.toFixed(2)}</p>
      <p>الحد الأقصى: {statistics.max}</p>
      <p>الحد الأدنى: {statistics.min}</p>
    </div>
  );
}

ملاحظة: لا تبالغ في استخدام useMemo. الحفظ نفسه له تكلفة إضافية. استخدمه فقط للحسابات المكلفة حقًا أو للحفاظ على تساوي المرجع.

useCallback: حفظ الوظائف

useCallback يعيد وظيفة callback محفوظة، مما يمنع إعادة العرض غير الضرورية للمكونات الفرعية:

import { useState, useCallback, memo } from 'react';

// مكون فرعي يتلقى callback
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  console.log(`تم عرض TodoItem ${todo.id}`);

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>حذف</button>
    </div>
  );
});

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'تعلم React', completed: false },
    { id: 2, text: 'بناء مشروع', completed: false },
    { id: 3, text: 'نشر التطبيق', completed: false }
  ]);
  const [filter, setFilter] = useState('all');

  // بدون useCallback - وظيفة جديدة في كل عرض
  // سيؤدي هذا إلى إعادة عرض TodoItem حتى مع React.memo
  const handleToggle = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // مع useCallback - نفس مرجع الوظيفة بين العروض
  const handleToggleMemoized = useCallback((id) => {
    setTodos(prevTodos => prevTodos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []); // deps فارغة - الوظيفة لا تتغير أبدًا

  const handleDelete = useCallback((id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  }, []);

  // وظيفة مع تبعيات
  const handleAddTodo = useCallback((text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  }, []); // يمكن أن تحتوي على تبعيات إذا لزم الأمر

  return (
    <div>
      <button onClick={() => setFilter('all')}>الكل</button>
      <button onClick={() => setFilter('active')}>نشط</button>
      <button onClick={() => setFilter('completed')}>مكتمل</button>

      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggleMemoized}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

أفضل ممارسة: استخدم useCallback عند تمرير callbacks إلى المكونات الفرعية المحفوظة. بدونه، ستعيد المكونات الفرعية العرض لأن مرجع callback يتغير.

دمج تقنيات التحسين

مثال عملي يجمع React.memo و useMemo و useCallback:

import { useState, useMemo, useCallback, memo } from 'react';

// مكون فرعي محفوظ
const DataRow = memo(({ item, onUpdate, onDelete }) => {
  console.log(`تم عرض الصف ${item.id}`);

  return (
    <tr>
      <td>{item.name}</td>
      <td>{item.value}</td>
      <td>
        <button onClick={() => onUpdate(item.id, item.value + 1)}>+</button>
        <button onClick={() => onDelete(item.id)}>حذف</button>
      </td>
    </tr>
  );
});

function OptimizedDataTable({ initialData }) {
  const [data, setData] = useState(initialData);
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState('name');

  // البيانات المفلترة والمرتبة المحفوظة
  const processedData = useMemo(() => {
    console.log('معالجة البيانات...');

    let result = data;

    // التصفية
    if (filter) {
      result = result.filter(item =>
        item.name.toLowerCase().includes(filter.toLowerCase())
      );
    }

    // الفرز
    result = [...result].sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      if (sortBy === 'value') return a.value - b.value;
      return 0;
    });

    return result;
  }, [data, filter, sortBy]);

  // callbacks محفوظة
  const handleUpdate = useCallback((id, newValue) => {
    setData(prevData =>
      prevData.map(item =>
        item.id === id ? { ...item, value: newValue } : item
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setData(prevData => prevData.filter(item => item.id !== id));
  }, []);

  return (
    <div>
      <input
        type="text"
        placeholder="تصفية حسب الاسم..."
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
      />

      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">فرز حسب الاسم</option>
        <option value="value">فرز حسب القيمة</option>
      </select>

      <table>
        <tbody>
          {processedData.map(item => (
            <DataRow
              key={item.id}
              item={item}
              onUpdate={handleUpdate}
              onDelete={handleDelete}
            />
          ))}
        </tbody>
      </table>
    </div>
  );
}

محلل React DevTools

استخدم محلل React DevTools لتحديد نقاط ضعف الأداء:

// تثبيت امتداد React DevTools للمتصفح
// Chrome: https://chrome.google.com/webstore (ابحث عن "React Developer Tools")
// Firefox: https://addons.mozilla.org/firefox/ (ابحث عن "React Developer Tools")

// استخدام المحلل برمجيًا
import { Profiler } from 'react';

function onRenderCallback(
  id,        // معرف المكون
  phase,     // "mount" أو "update"
  actualDuration, // الوقت المستغرق في العرض
  baseDuration,   // الوقت المقدر بدون الحفظ
  startTime,
  commitTime
) {
  console.log(`المكون ${id} - ${phase}`, {
    actualDuration,
    baseDuration,
    improvement: baseDuration - actualDuration
  });
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <TodoList />
      <DataTable />
    </Profiler>
  );
}

تمرين 1: تحسين قائمة المنتجات

المهمة: تحسين مكون قائمة المنتجات:

  • أنشئ قائمة من 100+ منتج مع البحث والتصفية
  • استخدم React.memo على مكون ProductCard
  • استخدم useMemo لحفظ النتائج المفلترة/المرتبة
  • استخدم useCallback لمعالجات الأحداث
  • قِس تحسين الأداء باستخدام محلل React DevTools

تمرين 2: إدارة دورة الحياة

المهمة: بناء مكون مؤقت مع تنظيف مناسب:

  • أنشئ مؤقتًا عكسيًا يتحدث كل ثانية
  • استخدم useEffect مع تنظيف مناسب (مسح الفاصل عند إلغاء التركيب)
  • أضف وظيفة الإيقاف/الاستئناف
  • منع تسريب الذاكرة عند إلغاء تركيب المكون
  • أضف تحديث عنوان المستند يظهر الوقت المتبقي

تمرين 3: لوحة تحكم بيانات معقدة

المهمة: بناء لوحة تحكم تحليلات محسّنة:

  • عرض الرسوم البيانية بحسابات مكلفة (المتوسط، الوسيط، الانحراف المعياري)
  • استخدم useMemo للحسابات الإحصائية
  • نفّذ المرشحات ومحددات نطاق التاريخ
  • احفظ مكونات الرسم البياني الفرعية بـ React.memo
  • قارن الأداء قبل وبعد التحسين

الخلاصة

في هذا الدرس، تعلمت تقنيات تحسين الأداء الأساسية:

  • دورة حياة المكون في المكونات الوظيفية مع useEffect
  • منع إعادة العرض غير الضرورية مع React.memo
  • حفظ الحسابات المكلفة مع useMemo
  • حفظ وظائف callback مع useCallback
  • دمج تقنيات التحسين للحصول على أقصى أداء
  • التحليل وقياس الأداء باستخدام React DevTools
  • أفضل الممارسات لمتى وكيفية تطبيق التحسينات

في الدرس التالي، سنستكشف حدود الأخطاء و Suspense وأنماط معالجة الأخطاء المتقدمة في React.