useCallback هو خطاف React يعيد نسخة مخزنة مؤقتًا من دالة لا تتغير بين عمليات التصيير حتى تتغير تبعياتها. على عكس الإعلان العادي عن دالة داخل المكون (الذي ينشئ دالة جديدة في كل تصيير)، يعمل useCallback على تثبيت المرجع للدالة، مما يمنع إعادة التصيير غير الضرورية للمكونات الفرعية المحسّنة عبر React.memo. وفقًا React Documentation (2025)، فإن useCallback مفيد فقط مع React.memo أو الخطافات التي تعتمد على مرجع ثابت.
الخلاصة
useCallback هو خطاف أضيف في React 16.8 يخزن الدالة مؤقتًا: يعيد نفس المرجع حتى تتغير التبعيات. بدون useCallback، كل إعلان دالة داخل المكون ينشئ كائن دالة جديد في كل تصيير. بالنسبة للقيم البدائية هذا غير ملحوظ، ولكن عند تمرير هذه الاستدعاءات إلى مكونات فرعية محسّنة بـ React.memo، كل مرجع جديد يسبب إعادة تصيير للمكون الفرعي.
من الناحية التركيبية، useCallback مكافئ لـ useMemo للدوال: useCallback(fn, deps) هو اختصار لـ useMemo(() => fn, deps). يخزن React الدالة المخزنة مؤقتًا في التخزين الداخلي لعقدة fiber ويقارن التبعيات في كل تصيير. إذا لم تتغير التبعيات (Object.is لكل عنصر)، يتم إرجاع الدالة السابقة.
وفقًا React Documentation — useCallback (2025)، لا يجب أن تلف كل دالة في useCallback. للخطاف تكلفته: استدعاء الخطاف، مقارنة التبعيات، وتخصيص ذاكرة لمصفوفة التبعيات. إذا كان المكون بسيطًا وليس لديه أشجار عميقة مع React.memo، فإن useCallback سيبطئ التطبيق فقط. يجب أن يكون التحسين قابلًا للقياس، وليس حدسيًا.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// مرجع ثابت — نفس الدالة حتى تتغير التبعيات
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
التخزين المؤقت في useCallback يعتمد على تخزين نتيجة استدعاء الدالة. يحتفظ React بالإغلاق (closure) الذي تم إنشاؤه في أول تصيير ويعيده في التصييرات اللاحقة طالما بقيت التبعيات دون تغيير. داخل عقدة fiber، كل استدعاء لـ useCallback ينشئ عقدة في القائمة المرتبطة للخطافات، حيث يتم تخزين التبعيات السابقة والقيمة المخزنة مؤقتًا.
مقارنة التبعيات تتم بدقة عبر Object.is — مقارنة سطحية بدون فحص عميق للكائنات أو المصفوفات. إذا كانت التبعية كائنًا أو مصفوفة، فسيتم اعتبار المرجع الجديد في كل تصيير تغييرًا. لذلك، يجب أن تحتوي مصفوفة التبعيات على قيم بدائية أو مراجع ثابتة (مثلًا من useRef أو useMemo).
وفقًا React Core Team — Optimization Guide (2024)، تشمل تكلفة التخزين المؤقت ثلاثة مكونات: تخصيص مصفوفة التبعيات في كل تصيير، تكرار ومقارنة العناصر عبر Object.is، والعبء المحتمل لجمع القمامة عند إعادة الإنشاء. بالنسبة لمكون يحتوي على مئات من أغلفة useCallback، يمكن أن يصبح هذا ملحوظًا — لذلك الانتقائية في استخدام الخطاف أمر بالغ الأهمية.
| السيناريو | بدون useCallback | مع useCallback |
|---|---|---|
| إنشاء الدالة | جديدة في كل تصيير | نفسها مع deps ثابتة |
| تمرير إلى React.memo | الابن يعيد التصيير | الابن لا يعيد التصيير |
| في مصفوفة useEffect | التأثير يعاد تشغيله | التأثير مستقر |
| العبء الإضافي | أدنى | مقارنة التبعيات + ذاكرة |
هناك اعتقاد خاطئ واسع الانتشار أن useCallback يحسن الأداء تلقائيًا. في الواقع، بشكل منعزل (بدون React.memo)، فإن useCallback يبطئ التطبيق قليلاً بسبب تكلفة مقارنة التبعيات. يحقق الخطاف فائدة حقيقية فقط في ثلاثة سيناريوهات: منع إعادة تصيير مكونات React.memo، تثبيت الاستدعاءات في useEffect، وتمرير الاستدعاءات إلى خطافات مخصصة تعتمد على مساواة المرجع.
القاعدة بسيطة: حتى تكتشف مشكلة أداء عبر React DevTools Profiler — لا تستخدم useCallback. أكد فريق React مرارًا أن التحسين المبكر هو أصل كل الشرور. اكتب أولاً كودًا نظيفًا بدون تخزين مؤقت، قم بالقياس، ابحث عن عنق الزجاجة في الملف الشخصي، وبعدها فقط أضف useCallback حيثما حقًا هناك حاجة.
// تحسين قابل للقياس: Child مغلف بـ React.memo
const Child = React.memo(({ onClick }) => {
console.log('إعادة تصيير Child');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('تم النقر');
}, []);
return <Child onClick={handleClick} />;
}
وفقًا Dan Abramov — Before You memo() (2024)، أكثر من 90% من حالات استخدام useCallback في المشاريع مفتوحة المصدر زائدة عن الحاجة. يقوم المطورون بلف كل دالة «احتياطًا» دون قياس التأثير. البديل: إذا كان المكون الفرعي ثقيلاً وإعادة تصييره مكلفة — فإن React.memo + useCallback مبرر. إذا كان المكون الفرعي خفيفًا — إعادة التصيير أرخص من مقارنة التبعيات.
السيناريو الأول — React.memo. إذا كان المكون الفرعي مغلفًا بـ React.memo ويستقبل دالة استدعاء كخاصية، بدون useCallback سيعيد المكون الفرعي التصيير في كل تصيير للأب، حتى لو لم تتغير بياناته الخاصة. useCallback يثبت المرجع، مما يسمح لـ React.memo بتخطي إعادة التصيير بشكل صحيح.
السيناريو الثاني — useEffect مع استدعاء في التبعيات. إذا تم تمرير دالة إلى مصفوفة تبعيات useEffect، كل مرجع جديد سيعيد تشغيل التأثير. useCallback يضمن أن المرجع مستقر، وأن التأثير يُنفذ فقط عندما تتغير البيانات الفعلية، وليس في كل تصيير. هذا مهم بشكل خاص للاشتراكات والطلبات.
// useCallback لتبعية useEffect مستقرة
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // مرجع ثابت، لا يعاد إنشاؤه أبدًا
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // التأثير يعمل فقط عندما يتغير props.id
الفرق الرئيسي بين useCallback و useMemo هو ما يخزنه كل منهما مؤقتًا. useCallback يخزن الدالة مؤقتًا: useCallback(fn, deps) يعيد fn (نفسها أو النسخة السابقة). useMemo يخزن نتيجة استدعاء دالة: useMemo(() => computeExpensive(a, b), [a, b]) يعيد القيمة المحسوبة، وليس دالة.
من الناحية الفنية، useCallback هو سكر نحوي فوق useMemo: useCallback(fn, deps) مكافئ لـ useMemo(() => fn, deps). هذه الصيغة موجودة فقط للقراءة — ليرى المطور بوضوح أنه يتم تخزين دالة وليس قيمة. لا يوجد فرق في الأداء بين useCallback و useMemo مع دالة — كلاهما يولد كودًا متطابقًا.
| الخطاف | يخزن مؤقتًا | الصيغة | الاستخدام |
|---|---|---|---|
| useCallback | الدالة (المرجع) | useCallback(fn, deps) | استدعاءات للمكونات الفرعية |
| useMemo | نتيجة الحساب | useMemo(() => value, deps) | حسابات مكلفة، تخزين الكائنات مؤقتًا |
// هذه متكافئة:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
الخطأ الأكثر شيوعًا هو اللف غير المجدي لجميع الدوال في useCallback بدون React.memo على المكونات الفرعية. إذا كان المكون الفرعي غير مغلف بـ React.memo، فسيظل يعيد التصيير في كل تصيير للأب، بغض النظر عما إذا كان مرجع الاستدعاء يتغير أم لا. useCallback بدون React.memo هو تكلفة بدون فائدة.
// ❌ غير مفيد: لا يوجد React.memo على الابن
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child لا يزال يعيد التصيير بدون React.memo
// ❌ إغلاق قديم: تبعية مفقودة
const handle = useCallback(() => {
console.log(count); // count دائمًا 0 — إغلاق قديم!
}, []);
// ✅ صحيح: تضمين التبعيات
const handle = useCallback(() => {
console.log(count);
}, [count]);
يتم حل مشكلة الإغلاق القديم (stale closure) في useCallback بتضمين جميع المتغيرات المستخدمة في مصفوفة التبعيات. eslint-plugin-react-hooks مع exhaustive-deps يتحقق تلقائيًا من أن جميع المتغيرات من جسم الاستدعاء موجودة في المصفوفة. إذا كان الاستدعاء يستخدم setState، الذي لا يتغير بين التصييرات، يمكن تضمينه بأمان في deps — يضمن React استقرار setState.
الأسئلة الشائعة
لا. useCallback له معنى فقط في ثلاث حالات: المكون الفرعي مغلف بـ React.memo، الدالة مستخدمة في مصفوفة تبعيات useEffect، أو الدالة تمرر إلى خطاف مخصص يعتمد على مساواة المرجع. في الحالات الأخرى، يضيف useCallback عبئًا بدون فائدة. يوصي فريق React بالكتابة أولاً بدون تحسينات وإضافتها بناءً على نتائج التنميط.
للمكونات البسيطة — دالة جديدة في كل تصيير أسرع قليلاً، لأن useCallback يستهلك موارد في مقارنة التبعيات وتخصيص المصفوفة. للمكونات ذات الأشجار العميقة من React.memo — يفوز useCallback بمنع إعادة تصيير آلاف العناصر الفرعية. قس وقارن بدلاً من التخمين — استخدم React DevTools Profiler للتقييم الموضوعي.
نعم، useCallback يعمل مع الدوال غير المتزامنة تمامًا كما مع الدوال المتزامنة. يخزن الخطاف الدالة نفسها، ويتم إرجاع النتيجة (Promise) في كل مرة يتم استدعاؤها. الدالة غير المتزامنة داخل useCallback هي نمط شائع لاستدعاءات تحميل البيانات المستقرة المستخدمة في useEffect: const fetchData = useCallback(async (id) => {...}, []).
استخدم React DevTools Profiler — يظهر أي المكونات تعيد التصيير ولماذا. للتحقق برمجيًا، أضف console.log أو استخدم useWhyDidYouUpdate — مكتبة تسجل سبب إعادة التصيير. الأسباب الرئيسية: تغيرت خاصية (بما في ذلك مرجع الاستدعاء)، تغيرت الحالة أو السياق. إذا لم يساعد useCallback — تحقق من أن جميع التبعيات محددة بشكل صحيح.
حتى بدون React.memo، useCallback يمكن أن يكون مفيدًا مع useMemo لقيمة السياق. إذا كنت تمرر كائنًا بدوال إلى Context.Provider، لف إنشاء الكائن في useMemo، وكل دالة في useCallback. هذا يمنع إعادة تصيير جميع مستهلكي السياق عند تغيير إحدى الدوال. ولكن لتمرير الاستدعاءات مباشرة في الخصائص بدون React.memo، لا فائدة من useCallback.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.