أضاف React 18 خطاف (hook) يُسمى useSyncExternalStore، وهو يتيح للمكونات قراءة البيانات من أي مصدر خارج شجرة React — مثل واجهات برمجة تطبيقات المتصفح (browser APIs)، أو WebSockets، أو المتاجر الخارجية (third-party stores) — دون المشكلات البصرية التي كانت تصاحب النمط القديم المعتمد على محاكاة تلك البيانات باستخدام useState و useEffect. إذا سبق لك أن رأيت وميضاً (flicker) في واجهة المستخدم لأن جزءاً منها عرض حجم نافذة قديماً بينما عكس جزء آخر الحجم الجديد بالفعل، فإن هذا الـ hook هو الحل الذي كنت تنتظره.

لماذا تحتاج الحالة الخارجية (external state) إلى hook جديد

لطالما كانت مكونات React قادرة على الاشتراك في أشياء توجد خارج نطاقها الخاص: كائن window ، أو متجر Redux، أو اتصال WebSocket. كان النهج التقليدي هو إعداد اشتراك داخل useEffect ، ودفع القيم الواردة إلى الحالة المحلية عبر setState ، وترك React يعيد عملية الـ render. يعمل هذا بشكل جيد عندما يقوم React بعملية الـ render بشكل متزامن (synchronously)، ولكن React 18 قدم الـ concurrent rendering، حيث قد يتم عمل render للمكون عدة مرات قبل أن يقوم المتصفح بالرسم (paint) فعلياً. في هذا الوضع، يمكن لنمط "المحاكاة في الحالة المحلية" أن يتسبب في حدوث tearing — أي أن أجزاء مختلفة من واجهة المستخدم تقرأ لقطات (snapshots) مختلفة لنفس القيمة الخارجية خلال دورة render واحدة.

تم بناء useSyncExternalStore لمنع حدوث الـ tearing. فهو يطلب من المصدر الخارجي snapshot (القيمة الحالية) ودالة subscribe (كيفية الإخطار بالتغييرات). يستدعي React دالة subscribe عند تحميل المكون (mount) ويقوم بإلغاء الاشتراك تلقائياً عند إزالته (unmount). وكلما تغيرت الـ snapshot التي تعيدها دالة getSnapshot ، يقوم React بجدولة عملية render تقرأ نفس الـ snapshot لشجرة المكونات بأكملها، مما يضمن الاتساق.

الدالتان المطلوبتان

الدالة ماذا تفعل
subscribe تستقبل callback يجب استدعاؤه كلما تغيرت القيمة الخارجية. وتُعيد دالة unsubscribe سيقوم React باستدعائها عند التنظيف (cleanup).
getSnapshot تُعيد القيمة الحالية من المصدر الخارجي. يجب أن تكون مستقرة مرجعياً (referentially stable) — أي نفس كائن الدالة في كل render — حتى يتمكن React من مقارنة الـ snapshots بشكل صحيح.

يبدو التنفيذ الأدنى لحالة الاتصال online في المتصفح كما يلي:

function useOnlineStatus() {
  return useSyncExternalStore(
    (onChange) => {
      window.addEventListener('online', onChange);
      window.addEventListener('offline', onChange);
      return () => {
        window.removeEventListener('online', onChange);
        window.removeEventListener('offline', onChange);
      };
    },
    () => navigator.onLine
  );
}

عندما يستدعي أي مكون useOnlineStatus() ، سيقوم React بإعادة عمل render له فقط عندما تتغير حالة navigator.onLine فعلياً، وسترى كل عملية render نفس القيمة.

قواعد تمنع الـ hook من التسبب في مشاكل

  1. أعد أصغر قطعة بيانات يحتاجها المكون.
    إذا كان المتجر الخارجي يحتوي على كائن كبير ولكن المكون يهتم بحقل واحد فقط، فأعد ذلك الحقل فقط. الإعادات الكبيرة تسبب عمليات render غير ضرورية.

  2. استخدم الـ Memoization لدالة getSnapshot.
    إذا قمت بإعادة إنشاء الدالة في كل render، سيعاملها React كمصدر جديد وقد يدخل في حلقة مفرغة (loop) إلى الأبد. قم بتغليفها في useCallback أو عرّفها خارج المكون.

  3. تجنب إرجاع كائنات جديدة في كل استدعاء.
    إرجاع كائن جديد (مثل { count: store.getCount() }) ينشئ مرجعاً جديداً في كل render، مما يجعل React يعتقد أن الـ snapshot قد تغيرت، ويؤدي ذلك إلى حلقة render لانهائية. استخدم القيم الأولية (primitives)، أو النصوص، أو الأرقام، أو الكائنات التي تم عمل memoization لها.

  4. لا تستخدمه لحالة المكون الداخلية.
    لا تزال useState هي الأداة المناسبة للبيانات التي تعيش فقط داخل المكون. يضيف useSyncExternalStore عبئاً إضافياً (overhead) ليس ضرورياً للحالة المحلية.

متى تلجأ إليه

  • واجهات برمجة تطبيقات المتصفح (Browser APIs) – حجم النافذة، استعلامات الوسائط (media queries)، navigator.onLine ، وحالة البطارية.
  • المتاجر المستقلة عن الإطارات (Framework-agnostic stores) – مثل Zustand، أو Redux، أو MobX، أو أي متجر مخصص يوفر واجهة برمجة تطبيقات للاشتراك (subscription API).
  • المصادر القابلة للتغيير التي يتم تحديثها خارج React – رسائل WebSocket، وأحداث تغيير IndexedDB، وإشعارات Service Worker.

إذا كان مصدر بياناتك يعيش بالفعل داخل React (على سبيل المثال، حالة مكون أب)، فالتزم باستخدام useState أو context.

ماذا يقول المجتمع

أفاد المستخدمون الأوائل أن useSyncExternalStore يقضي على الوميض الذي كانوا يلاحظونه عند تغيير حجم النوافذ في إعدادات الـ concurrent-rendering. قامت بعض المكتبات بالفعل بتحويل خطافاتها الداخلية إلى هذه الـ API، مما يعد بسلوك أكثر قابلية للتنبؤ عبر إصدارات React المختلفة. المقابل هو عبء ذهني أعلى قليلاً: يجب على المطورين التفكير في استقرار الدالتين وتجنب إرجاع كائنات جديدة في كل استدعاء.

ما الذي يجب مراقبته لاحقاً

قد تعمل إصدارات React القادمة على إحكام القواعد المتعلقة بـ useSyncExternalStore ، وربما إضافة أدوات مساعدة مدمجة لواجهات برمجة تطبيقات المتصفح (browser APIs) الشائعة. تابع مدونة React الرسمية لمعرفة أي إعلانات عن إيقاف دعم نمط محاكاة useEffect القديم. في هذه الأثناء، يُعد هذا الـ hook مستقراً وجزءاً من واجهة برمجة التطبيقات (public API) العامة، لذا يمكنك إعادة صياغة (refactor) الكود الحالي الخاص بالحالة الخارجية (external-state) بأمان.

تجربة سريعة

يتوفر عرض توضيحي مباشر يستعرض مزامنة حجم النافذة، والكشف عن حالة الاتصال (online/offline)، ومتجر Zustand بسيط على https://usesyncexternalstore.vercel.app/. الكود المصدري، مع التعليقات الكاملة، متاح على https://github.com/dev48v/usesyncexternalstore.

الخلاصة: يوفر useSyncExternalStore لـ React جسراً موثوقاً لأي بيانات قابلة للتغيير (mutable data) تعيش خارج دورة الرندرة (render cycle) الخاصة بها، مما يمنع حدوث التمزق (tearing) وعمليات الرندرة غير الضرورية. قم بتطبيقه على المصادر الخارجية، وحافظ على الـ snapshot في حدها الأدنى وبشكل مستقر، واترك لـ React القيام بالعمل الشاق لضمان الاتساق.