يشترك useSyncExternalStore بمكوّن في بيانات تعيش خارج React ويعيد عرضه كلما تغيرت تلك البيانات. تعطيه دالتين: subscribe، التي تخبر React كيف تستمع إلى التغييرات، وgetSnapshot، التي تُرجع القيمة الحالية.
لا يتشارك مكوّنا Display أي props ولا سياق، ومع ذلك يتحدثان معًا، لأن كليهما مشترك في المخزن نفسه. يستدعي الزر دالة عادية، لا دالة ضبط من React. أضف <Display name="Sidebar" /> ثالثًا فينضم دون أي تغيير آخر.
الصيغة
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
- تبدأ
subscribe(callback)الاستماع، وتستدعيcallbackكلما احتمل أن تكون البيانات قد تغيرت، وتُرجع دالة لإلغاء الاشتراك. تستدعيها React بعد تركيب المكوّن وتستدعي الدالة المُرجعة عند إزالته. - تُرجع
getSnapshot()القيمة الحالية. تستدعيها React أثناء كل عرض وبعد كل إشعار، ثم تقارن النتيجة بالأخيرة باستخدامObject.is. قيمة نفسها، لا عرض. - تُرجع
getServerSnapshot()(اختيارية) القيمة التي تُستخدم على الخادم وأثناء الإماهة.
عرّف subscribe خارج المكوّن، أو أبقِها ثابتة بـ useCallback. إذا مررت دالة subscribe جديدة في كل عرض، تلغي React الاشتراك وتشترك من جديد في كل مرة.
واجهات المتصفح كمخازن
كل ما له قيمة حالية ويطلق حدثًا عندما تتغير القيمة يناسب هذا الشكل. وnavigator.onLine مع الحدثين online وoffline هو الحالة الكلاسيكية.
أطفئ شبكتك (أو بدّل إلى Offline في أدوات المطور في متصفحك) فيتغير النص دون إعادة تحميل. يقول الوسيط الثالث «افترض الاتصال» عند العرض على الخادم، حيث لا يوجد navigator. وتغليف استدعاء الخطاف في useOnlineStatus يجعله خطافًا مخصصًا يستطيع أي مكوّن استخدامه.
ويعمل عرض النافذة بالطريقة نفسها:
أضف بضع نسخ وغيّر حجم النافذة: تُظهر كل نسخة الرقم نفسه في اللحظة نفسها. لكل نسخة مستمعها الخاص، وكل منها تقرأ العرض أثناء العرض، فلا تتأخر أي منها بإطار أبدًا.
يجب أن تُرجع getSnapshot قيمة مخزنة مؤقتًا
تستدعي React الدالة getSnapshot كثيرًا وتقارن النتائج بالمرجع. الدالة التي تبني كائنًا أو مصفوفة جديدة في كل استدعاء تبدو دائمًا كتغيير:
// Broken: a new object on every call
function getSnapshot() {
return { count: store.count, user: store.user };
}
// Also broken: filter returns a new array every time
function getSnapshot() {
return store.todos.filter((t) => !t.done);
}
تعرض React، وتستدعي getSnapshot، فتحصل على قيمة «مختلفة»، فتعيد العرض، وهكذا، حتى تتوقف بـ "Maximum update depth exceeded". وفي التطوير تسجّل React أولًا أيضًا "The result of getSnapshot should be cached to avoid an infinite loop". أما بناء الإنتاج، مثل المعاينة هنا، فيتخطى هذا التحذير ولا يبلّغ إلا عن الخطأ الأخير كرمز قصير (Minified React error #185).
الإصلاح هو إبقاء البيانات غير قابلة للتعديل في المخزن: استبدل الكائن عندما يتغير، وأرجع المرجع المخزن كما هو.
تُرجع getSnapshot الكائن state نفسه حتى تستبدله add، فيُعرض المكوّن مرة لكل تغيير. ويحدث الترشيح في المكوّن، بعد قراءة اللقطة، وهذا آمن. وللترشيح داخل المخزن بدلًا من ذلك، احسب المصفوفة المرشحة عندما تتغير البيانات وخزّنها، لتتمكن getSnapshot من إرجاع النسخة المخزنة.
getServerSnapshot والإماهة
على الخادم لا توجد نافذة ولا navigator ولا اشتراك. يخبر الوسيط الثالث React بما تعرضه هناك:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
تستخدم React أيضًا getServerSnapshot للعرض الأول في المتصفح عندما تُميه HTML الخادم، ليتطابق الاثنان. وبعد الإماهة مباشرة تقرأ getSnapshot، وإذا اختلفت القيمة الحقيقية، تعيد العرض بها. ومن دون الوسيط الثالث، يرمي العرض على الخادم "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." إذا كانت هناك حدود <Suspense> فوق المكوّن، يرسل الخادم المحتوى البديل لتلك الحدود ويعرض المتصفح محتواها بدلًا منه؛ ومن دون حدود، يفشل العرض على الخادم.
useSyncExternalStore مقابل useEffect وuseState
يمكنك الاشتراك بتأثير:
function useOnlineStatus() {
const [online, setOnline] = useState(true);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return online;
}
هذا يعمل، لكن فيه نقطتا ضعف. يُظهر العرض الأول دائمًا التخمين الابتدائي، وتصل القيمة الحقيقية بعد عرض واحد، بعد أن يعمل التأثير. ومع العرض المتزامن (أثناء انتقال، مثلًا)، قد توقف React عرضًا في منتصفه؛ وإذا تغير المخزن أثناء التوقف، فقد تُظهر المكوّنات المعروضة قبله وبعده قيمًا مختلفة. يُسمى هذا التضارب tearing (التمزق). أما useSyncExternalStore فيقرأ القيمة أثناء العرض ويجعل React تعيد العرض بشكل متزامن إذا تغير المخزن، فيرى كل مكوّن القيمة نفسها.
استخدمه عندما تعيش البيانات خارج React: وحدة المخزن الخاصة بك، أو واجهة متصفح، أو مكتبة خارجية. معظم مكتبات الحالة (Redux وZustand وغيرهما) تستدعيه نيابة عنك داخل خطافاتها. أما للبيانات التي تنتمي إلى مكوّناتك، فما زالت useState وuseReducer والسياق هي الأدوات المناسبة.
الأسئلة الشائعة
فيمَ يُستخدم useSyncExternalStore؟
لقراءة بيانات لا تملكها React ويمكن أن تتغير من تلقاء نفسها: مخزن مكتوب خارج React، أو مكتبة حالة خارجية، أو قيمة من المتصفح مثل navigator.onLine أو عرض النافذة. يُعاد عرض المكوّن كلما أخبر المخزن React بأنه تغيّر.
ماذا تفعل subscribe وgetSnapshot؟
تبدأ subscribe(callback) الاستماع إلى المخزن، وتستدعي callback مع كل تغيير، وتُرجع دالة توقف الاستماع. وتُرجع getSnapshot() القيمة الحالية. تستدعي React الدالة getSnapshot أثناء العرض وبعد كل إشعار، ولا تعيد العرض إلا إذا تغيرت القيمة بحسب Object.is.
لماذا يجب أن تُرجع getSnapshot قيمة مخزنة مؤقتًا؟
تقارن React نتيجة كل استدعاء لـ getSnapshot بالسابقة. إذا أرجعت كائنًا أو مصفوفة جديدة في كل مرة، ترى React تغييرًا دائمًا، فتعيد العرض، وتستدعي getSnapshot من جديد، وتدور في حلقة حتى ترمي "Maximum update depth exceeded". أرجع المرجع نفسه حتى تتغير البيانات فعلًا.
ما هي getServerSnapshot؟
الوسيط الثالث الاختياري. تُرجع القيمة التي تُستخدم أثناء العرض على الخادم وأثناء الإماهة في المتصفح، لينتج الاثنان HTML نفسه. ومن دونها يرمي المكوّن "Missing getServerSnapshot" على الخادم، ويُعرض المحتوى تحت أقرب حدود <Suspense> في المتصفح بدلًا من ذلك.
هل أستخدم useSyncExternalStore أم useEffect مع useState؟
للاشتراك في بيانات خارجية، فضّل useSyncExternalStore. فهو يقرأ القيمة أثناء العرض، فيكون العرض الأول صحيحًا أصلًا ويرى كل مكوّن القيمة نفسها حتى أثناء العرض المتزامن. أما useEffect مع useState فيعرض مرة بقيمة قديمة وقد يُظهر لوهلة قيمًا مختلفة في مكوّنات مختلفة.