يتيح useSyncExternalStore للمكون قراءة البيانات التي توجد خارج React دون المخاطرة بحدوث خلل بصري قد تسببه الرندرة المتزامنة (concurrent rendering). وهو يحل محل النمط القديم المتمثل في سحب القيم الخارجية إلى الحالة المحلية باستخدام useEffect؛ وهو نمط قد يتسبب في حدوث "التمزق" (tearing) – أي ظهور أجزاء من واجهة المستخدم بقيم مختلفة خلال عملية الرندرة نفسها.
لماذا يفشل النمط القديم في ظل الرندرة المتزامنة
قبل React 18، كانت الطريقة الشائعة هي:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
يتم تشغيل الـ effect بعد رندرة المكون. عندما يقرر محرك الرندرة المتزامن في React إيقاف الرندرة مؤقتًا، أو إلغاءها، أو إعادة تشغيلها، فقد يتم تشغيل الـ effect في نقطة زمنية مختلفة عن واجهة المستخدم التي تقرأ الحالة. والنتيجة هي عدم تطابق: قد يقرأ أحد المكونات القيمة القديمة، بينما يقرأ مكون آخر القيمة الجديدة، فتظهر الشاشة بيانات غير متسقة لفترة وجيزة. هذا "التمزق" لا يمثل مشكلة في العالم المتزامن تمامًا، ولكنه يصبح خطأً مرئيًا عندما تقوم React برندرة إصدارات متعددة من واجهة المستخدم بالتوازي.
كيف يحل useSyncExternalStore المشكلة
يتطلب الـ hook دالتين (callbacks):
- subscribe – لتسجيل مستمع (listener) وإرجاع دالة لإزالته.
- getSnapshot – لإرجاع القيمة الحالية للمصدر الخارجي.
تقوم React باستدعاء subscribe عند تحميل المكون (mount) وتقوم بإلغائه عند إلغاء تحميله (unmount). وخلال كل عملية رندرة، تقوم باستدعاء getSnapshot وتضمن أن جميع المكونات التي تستخدم نفس المتجر الخارجي تقرأ نفس اللقطة (snapshot) تمامًا. ولأن عملية القراءة تحدث داخل مرحلة الرندرة، يمكن للمحرك تنسيق التحديثات عبر الشجرة بأكملها، مما يقضي على التمزق.
تقوم مكتبات مثل Zustand و Redux بالفعل بتغليف متاجرها باستخدام هذا الـ hook، ولهذا السبب تعمل مباشرة مع ميزات الرندرة المتزامنة في React 18.
قواعد لمنع الـ hook من التصرف بشكل خاطئ
- أرجع فقط ما يحتاجه المكون – الكائنات الكبيرة تجبر React على مقارنة الهياكل العميقة عند كل تحديث. اللقطة (snapshot) الضيقة تجعل اكتشاف التغييرات غير مكلف.
- الحفاظ على الهوية المرجعية (referential identity) – إذا لم تتغير القيمة الخارجية، يجب أن تعيد getSnapshot نفس المرجع للكائن. إرجاع كائن جديد في كل استدعاء يؤدي إلى حلقة رندرة لانهائية لأن React ترى تغييرًا في كل عملية رندرة.
- قصر الاستخدام على المصادر الخارجية – تنتمي حالة المكون المحلية إلى
useStateأوuseReducer. استخدام الـ hook الخاص بالمتاجر الخارجية للقيم الداخلية البحتة يضيف تعقيدًا غير ضروري.
متى تلجأ إلى useSyncExternalStore
- تغليف واجهات برمجة تطبيقات المتصفح (حجم النافذة، حالة الاتصال بالشبكة، استعلامات الوسائط).
- ربط متجر JavaScript عادي لا يعرف شيئًا عن React.
- استبدال تمرير الخصائص (prop-drilling) بمصدر عالمي للقراءة فقط يشترك فيه العديد من المكونات.
الحجة المضادة
لا يزال نمط useState + useEffect يعمل مع البيانات البسيطة التي تتغير بشكل غير متكرر، خاصة عندما لا يقوم التطبيق بتفعيل ميزات الرندرة المتزامنة. إنه نهج من مستوى أدنى يجده بعض المطورين أسهل في الفهم لأن منطق الاشتراك (subscription) موجود بشكل صريح في الـ effect. ومع ذلك، فإن المقابل هو خطر حدوث التمزق بمجرد تشغيل الرندرة المتزامنة. بالنسبة للمشاريع التي تستخدم بالفعل قدرات React 18 الجديدة، فإن الأمان الإضافي الذي يوفره useSyncExternalStore يفوق الحد الأدنى من الكود الإضافي (boilerplate).
ما يجب مراقبته لاحقًا
- تأكد من أن getSnapshot في المتجر الخاص بك يعيد مرجعًا مستقرًا؛ يمكن لعمل
console.logسريع للمرجع أثناء التطوير أن يكشف عن إعادة إنشاء الكائنات عن طريق الخطأ. - حافظ على خفة دالة الاشتراك (subscription function) – يجب أن تتم المهام الثقيلة داخل المصدر الخارجي، وليس في دالة الاستدعاء الممررة إلى React.
- اختبر المكونات في وضع الرندرة المتزامن لـ React (على سبيل المثال، باستخدام
actمعReactDOM.createRoot) للتأكد من عدم ظهور أي تمزق.
يعرض العرض التوضيحي الحي الـ hook أثناء العمل، حيث يقوم بمزامنة عداد بسيط مخزن خارج React وتحديث مكونات متعددة دون حدوث خلل بصري. العرض متاح على https://usesyncexternalstore.vercel.app/. الكود المصدري موجود على GitHub، وهناك مقال أطول يشرح تفاصيل التنفيذ.
الخلاصة: في عالم يمكن لـ React فيه رندرة أجزاء من واجهة المستخدم بترتيب غير منظم، يعد useSyncExternalStore الجسر الموثوق بين البيانات الخارجية ورندرة المكونات. فهو يحافظ على الاتساق البصري، ويتوسع ليشمل المتاجر العالمية، وهو مدمج بالفعل في مكتبات إدارة الحالة الشهيرة. اعتمد عليه أينما كنت بحاجة إلى قراءة حالة غير تابعة لـ React، وحافظ على صغر واستقرار اللقطات (snapshots) لجني كامل فوائده.
