🚀 احتراف useCallback: كيف تمنع إعادة إنشاء الدوال في React؟

اليوم سنقوم بحل مشكلة شائعة جداً يواجهها المطورون في React، وهي مشكلة "إعادة إنشاء الدوال" (Function Recreation) التي قد تؤدي إلى بطء في أداء التطبيق.

في الدروس السابقة، تعلمنا كيف تعمل المكونات (Components) وكيف يتم إعادة رندرتها (Re-render) عند تغير الحالة (State). لكن هل كنت تعلم أن كل دالة تُكتب داخل المكون يتم إنشاؤها من جديد في كل مرة يتم فيها تحديث المكون؟

هنا يأتي دور الـ Hook السحري: useCallback.


❓ ما هو useCallback بالضبط؟

ببساطة، useCallback هو "خزانة" يقوم React بوضع الدالة بداخلها. بدلاً من أن يقوم React بإنشاء نسخة جديدة من الدالة في كل مرة يتم فيها تحديث الصفحة، يقوم useCallback بإرجاع نفس النسخة من الدالة طالما لم تتغير القيم التي تعتمد عليها.

هذه العملية تسمى في البرمجة Memoization (التذكر)، وهي تعني أننا نحفظ النتيجة بدلاً من إعادة حسابها أو إنشائها من الصفر.


⚠️ لماذا نحتاج إلى useCallback؟ (المشكلة)

تخيل أن لديك مكوناً أب (Parent) ومكوناً ابناً (Child). إذا قمت بتعريف دالة في المكون الأب ومررتها كـ prop للمكون الابن، فإن المكون الابن سيعتقد أن الدالة قد تغيرت في كل مرة يتم فيها تحديث الأب، مما يؤدي إلى إعادة رندرة المكون الابن حتى لو لم يتغير شيء فيه! 📉

هذا يسبب استهلاكاً غير ضروري لموارد الجهاز ويجعل التطبيق يبدو ثقيلاً.


🛠️ كيف نستخدم useCallback خطوة بخطوة؟

طريقة كتابة useCallback تشبه إلى حد كبير useEffect من حيث الهيكل. تأخذ مدخلين:

  1. الدالة التي تريد حفظها.
  2. مصفوفة الاعتمادات (Dependency Array) التي تحدد متى يجب تحديث هذه الدالة.

المثال التطبيقي: 💻

لنقم ببناء مثال بسيط جداً: عداد (Counter) وزر يقوم بزيادة القيمة، وسنمرر دالة الزيادة إلى مكون فرعي.

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

// مكون فرعي لزر الزيادة
const IncrementButton = React.memo(({ increment }) => {
  console.log("Child Button Rendered! 📢"); // لمراقبة عدد مرات الرندرة
  return <button onClick={increment}>Increase Count</button>;
});

function CounterApp() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // استخدام useCallback لمنع إعادة إنشاء الدالة في كل رندرة
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // مصفوفة فارغة تعني أن الدالة ستنشأ مرة واحدة فقط عند بداية التشغيل

  return (
    <div style={{ padding: '20px' }}>
      <h1>Count: {count}</h1>
      
      {/* هذا الزر يستخدم الدالة المحفوظة */}
      <IncrementButton increment={increment} />

      <div style={{ marginTop: '20px' }}>
        <input 
          type="text" 
          value={text} 
          onChange={(e) => setText(e.target.value)} 
          placeholder="Type something..."
        />
        <p>Typing: {text}</p>
      </div>
    </div>
  );
}

export default CounterApp;

🔍 تحليل الكود: ماذا حدث هنا؟

  1. React.memo: استخدمناها في المكون IncrementButton لكي لا يعيد الرندرة إلا إذا تغيرت الـ props الممرة له.
  2. useCallback: قمنا بتغليف دالة increment.
    • إذا لم نستخدم useCallback $\rightarrow$ في كل مرة تكتب فيها في حقل الإدخال (input) وتتغير حالة text $\rightarrow$ سيتم إعادة إنشاء دالة increment $\rightarrow$ سيعيد المكون IncrementButton الرندرة (وهذا خطأ لأن الزر لا علاقة له بالنص!).
    • باستخدام useCallback $\rightarrow$ يدرك React أن الدالة لم تتغير، وبالتالي لا يعيد رندرة IncrementButton أبداً إلا إذا تغيرت الاعتمادات.

💡 متى تستخدم useCallback؟ (قاعدة ذهبية)

لا تستخدم useCallback مع كل دالة في مشروعك! لأن عملية "الحفظ" نفسها تستهلك ذاكرة. استخدمها فقط في الحالات التالية:

  1. عندما تمرر دالة كمكون prop إلى مكون ابن مُغلف بـ React.memo.
  2. عندما تكون الدالة مُستخدمة كـ dependency (اعتماد) في useEffect أو useMemo.

🏁 ملخص الدرس

  • useCallback يحفظ الدالة من إعادة الإنشاء عند كل رندرة.
  • يساعد في تحسين الأداء عن طريق تقليل عمليات إعادة الرندرة غير الضرورية للمكونات الأبناء.
  • يعمل جنباً إلى جنب مع React.memo لتحقيق أقصى استفادة.
  • يأخذ مصفوفة اعتمادات تحدد متى يتم تحديث الدالة.

⏭️ ماذا سنتعلم في الدرس القادم؟

الآن بعد أن تعلمنا كيف نحفظ الدوال باستخدام useCallback لنمنع إعادة حسابها، قد نتساءل: ماذا لو كان لدينا عملية حسابية معقدة تأخذ وقتاً طويلاً ونريد حفظ نتيجتها بدلاً من إعادة حسابها؟

في الدرس القادم سنتعرف على الـ Hook المكمل له وهو: useMemo وكيف يختلف عن useCallback. نراك هناك! 👋