Menu
flag Ar iconالعربيةdown icon

الخطاف useCallback في React: متى يفيد فعلًا

يُرجع useCallback الدالة نفسها بين مرات العرض حتى تتغير اعتمادياتها. ولا يفيد إلا عندما تُمرَّر تلك الدالة إلى ابن ملفوف بـ memo أو تُستخدم كاعتمادية لتأثير. تعلّم الصيغة، والفرق عن useMemo، والأخطاء التي تتجنبها.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

يخزّن useCallback دالة بين مرات العرض: يُرجع كائن الدالة نفسه حتى تتغير إحدى اعتمادياته. ولا يهم ذلك إلا عندما يقارن شيء ما الدالة بالمرجع، وهذا يعني عمليًا ابنًا ملفوفًا بـ memo أو تأثيرًا يسرد الدالة كاعتمادية.

الزران محفوظان ويفعلان الشيء نفسه. بدّل السمة وراقب Console: لا يظهر إلا render Inline function، لأن saveInline دالة جديدة في كل عرض، فيرى memo prop متغيرة. أما saveCached فهي الدالة نفسها في كل مرة، فيُتخطى ذلك الزر.

ما الذي يخزّنه

const cachedFn = useCallback(fn, dependencies);
  • fn هي الدالة التي تريد الاحتفاظ بها. لا تستدعيها React؛ بل تُرجعها إليك.
  • تسرد dependencies كل قيمة من المكوّن تقرؤها fn. وفي كل عرض تقارنها React بالسابقة باستخدام Object.is.
  • إذا لم يتغير شيء، تحصل على الدالة من العرض السابق. وإذا تغيّر شيء، تحصل على الدالة التي مررتها هذه المرة، وتحتفظ بها React للمرة القادمة.

لاحظ ما يعنيه هذا: الدالة السهمية داخل useCallback ما زالت تُنشأ في كل عرض، لأنها مجرد وسيط. لا يوفر useCallback ذلك العمل. مهمته الوحيدة هي إعادة دالة أقدم مطابقة ليبقى المرجع نفسه.

useCallback مقابل useMemo

هما الآلية نفسها. يخزّن useMemo نتيجة استدعاء دالة، ويخزّن useCallback الدالة نفسها:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

استخدم useCallback للدوال لأنه أوضح، وuseMemo للقيم المحسوبة مثل قائمة مرشحة أو كائن خيارات.

التخلص من الاعتماديات بدوال التحديث

يجب أن تسرد الدالة المخزنة كل قيمة حالة تقرؤها، وفي كل مرة تتغير فيها تلك الحالة تتغير الدالة أيضًا. وفي قائمة من الصفوف المحفوظة هذه مشكلة: دالة remove تقرأ todos تتغير كلما تغيرت todos، فيُعاد عرض كل صف متبقٍ بعد كل حذف. استخدم صيغة دالة التحديث في دالة الضبط حتى لا تقرأ الدالة todos إطلاقًا.

احذف صفًا: تبقى Console صامتة، لأن الصفوف الأخرى حصلت على كائن todo نفسه ودالة remove نفسها كما من قبل. غيّر الدالة إلى useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) وجرّب مرة أخرى: الآن يسجّل كل صف متبقٍ عرضًا بعد كل حذف.

الدوال كاعتماديات للتأثير

التأثير الذي يستدعي دالة معلنة في المكوّن عليه أن يسرد تلك الدالة كاعتمادية. إذا كانت الدالة جديدة في كل عرض، يعمل التأثير بعد كل عرض. ويُبقيها useCallback ثابتة حتى تتغير مدخلاتها الخاصة:

كتابة رسالة لا تعيد الاتصال. وتبديل الغرف يسجّل قطع الاتصال بالغرفة القديمة والاتصال بالجديدة. احذف مغلّف useCallback (واحتفظ بالدالة السهمية) فتقطع كل ضغطة مفتاح الاتصال وتعيده.

لكن هناك إصلاحًا أبسط هنا: انقل الدالة إلى داخل التأثير. عندها يعتمد التأثير على roomId مباشرة ولا تحتاج أي دالة إلى الثبات.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

فضّل ذلك عندما لا يستخدم الدالة إلا التأثير. واحتفظ بـ useCallback للدوال التي تمررها أيضًا إلى الأسفل أو تُرجعها من خطاف مخصص، حيث قد يضعها المستدعون في مصفوفات اعتمادياتهم الخاصة.

التحقق مما إذا تغيرت دالة

عندما يظل ابن محفوظ يُعاد عرضه ولست متأكدًا من السبب، تحقق مما إذا كانت الـ prop الدالة هي فعلًا الكائن نفسه بين مرات العرض. احتفظ بالسابقة في مرجع وقارن:

انقر Count: تقول Console increment changed: false، لأن step لم تتغير. وانقر Step فتقول true: step اعتمادية، فأعادت React الدالة الجديدة. والدالة التي تتغير في كل عرض تعني عادة أن إحدى الاعتماديات نفسها جديدة في كل عرض، مثل كائن أو دالة أخرى معلنة في المكوّن.

useCallback في الخطافات المخصصة

الخطاف المخصص الذي يُرجع دوالًا مكان مناسب لـ useCallback حتى عندما لا تعرف من سيستدعيه. قد يمرر المكوّن الذي يستخدم الخطاف تلك الدوال إلى ابن محفوظ أو يسردها في تأثير، ولا يستطيع جعلها ثابتة من الخارج. تغليفها داخل الخطاف يعطي كل مستدعٍ مرجعًا ثابتًا افتراضيًا:

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

الخطأ الشائع: تغليف كل شيء

useCallback حول معالج يذهب مباشرة إلى <button> أو <input> لا يفعل شيئًا مفيدًا. لا تتخطى React أي عمل لعنصر DOM لأن معالجه بقي نفسه، والمكوّن غير الملفوف بـ memo يُعرض على أي حال عندما يُعرض أبوه.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

كل useCallback إضافي يضيف مصفوفة اعتماديات قد يخطئ فيها أحدهم، والاعتمادية القديمة خطأ حقيقي (تظل الدالة تقرأ حالة قديمة). أضفه عندما يكون لديك ابن محفوظ أو مصفوفة اعتماديات في الطرف المستقبل، لا افتراضيًا.

يحفظ React Compiler 1.0، عندما يستخدمه البناء لديك، الدوال والقيم تلقائيًا، ما يلغي الحاجة إلى معظم استدعاءات useCallback المكتوبة يدويًا. ومن دون المترجم، ما زالت القواعد أعلاه تنطبق، وما زال useCallback اليدوي شائعًا في الكود الموجود.

الأسئلة الشائعة

ماذا يفعل useCallback؟

يُرجع كائن الدالة نفسه في كل عرض حتى تتغير إحدى اعتمادياته. لا يمنع إنشاء الدالة؛ بل يعيد الدالة من عرض سابق بدلًا من الجديدة.

متى أستخدم useCallback؟

عندما تمرر دالة إلى ابن ملفوف بـ memo، أو عندما تكون الدالة اعتمادية لتأثير أو لخطاف آخر. في الحالتين ستُبطل دالة جديدة في كل عرض التحسين أو تعيد تشغيل التأثير.

ما الفرق بين useCallback وuseMemo؟

يخزّن useCallback(fn, deps) الدالة fn نفسها. ويخزّن useMemo(() => value, deps) القيمة التي تُرجعها دالة. وuseCallback(fn, deps) هي بالضبط useMemo(() => fn, deps).

هل أغلّف كل دالة بـ useCallback؟

لا. إذا ذهبت الدالة إلى عنصر DOM عادي أو إلى مكوّن غير محفوظ، فإن المرجع الثابت لا يغيّر شيئًا، ومصفوفات الاعتماديات الإضافية تجعل الكود أصعب قراءة وأسهل خطأ.

هل يجعل useCallback مكوّني أسرع؟

ليس بذاته. لا يفيد إلا عندما يقارن شيء في الأسفل الدالة بالمرجع، مثل memo أو مصفوفة اعتماديات تأثير.

رسم توضيحي للغات البرمجة في Coddy

تعلّم البرمجة مع Coddy

ابدأ الآن