useSyncExternalStore רושם קומפוננטה לנתונים שחיים מחוץ ל-React ומרנדר אותה שוב בכל פעם שהנתונים האלה משתנים. נותנים לו שתי פונקציות: subscribe, שאומרת ל-React איך להאזין לשינויים, ו-getSnapshot, שמחזירה את הערך הנוכחי.
לשתי קומפוננטות ה-Display אין props משותפים ואין קונטקסט, ובכל זאת שתיהן מתעדכנות יחד, כי שתיהן רשומות לאותו store. הכפתור קורא לפונקציה רגילה, לא ל-setter של React. הוסיפו <Display name="Sidebar" /> שלישית והיא מצטרפת בלי שום שינוי אחר.
התחביר
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)מתחיל להאזין, קורא ל-callbackבכל פעם שהנתונים אולי השתנו, ומחזיר פונקציית ביטול הרשמה. React קוראת לו אחרי שהקומפוננטה עושה mount, וקוראת לפונקציה שהוחזרה ב-unmount.getSnapshot()מחזיר את הערך הנוכחי. React קוראת לו בכל רינדור ואחרי כל התראה, ואז משווה את התוצאה לקודמת בעזרתObject.is. אותו ערך, אין רינדור.getServerSnapshot()(אופציונלי) מחזיר את הערך לשימוש בשרת ובזמן hydration.
הגדירו את subscribe מחוץ לקומפוננטה, או שמרו אותו יציב עם useCallback. אם מעבירים פונקציית subscribe חדשה בכל רינדור, React מבטלת את ההרשמה ונרשמת שוב בכל פעם.
APIs של הדפדפן כ-stores
כל דבר שיש לו ערך נוכחי ושמפעיל אירוע כשהערך משתנה מתאים לצורה הזו. navigator.onLine עם האירועים online ו-offline הוא המקרה הקלאסי.
כבו את הרשת (או עברו ל-Offline בכלי המפתחים של הדפדפן) והטקסט משתנה בלי רענון. הארגומנט השלישי אומר "הנח שיש חיבור" כשמרנדרים בשרת, שבו אין navigator. עטיפת הקריאה להוק ב-useOnlineStatus הופכת אותה ל-הוק מותאם אישית שכל קומפוננטה יכולה להשתמש בו.
רוחב החלון עובד באותה צורה:
הוסיפו כמה עותקים ושנו את גודל החלון: כל עותק מציג את אותו מספר באותו רגע. לכל עותק יש listener משלו, וכל אחד קורא את הרוחב בזמן הרינדור, כך שאף אחד מהם אף פעם לא מפגר בפריים.
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". build של פרודקשן, כמו התצוגה המקדימה כאן, מדלג על האזהרה הזו ומדווח רק על השגיאה הסופית כקוד קצר (Minified React error #185).
התיקון הוא להחזיק את הנתונים ב-store כ-immutable: להחליף את האובייקט כשהוא משתנה, ולהחזיר את ההפניה השמורה כמו שהיא.
getSnapshot מחזיר את אותו אובייקט state עד ש-add מחליף אותו, כך שהקומפוננטה מתרנדרת פעם אחת לכל שינוי. הסינון קורה בקומפוננטה, אחרי שה-snapshot נקרא, וזה בטוח. כדי לסנן בתוך ה-store במקום, חשבו את המערך המסונן כשהנתונים משתנים ושמרו אותו, כך ש-getSnapshot יוכל להחזיר את העותק השמור.
getServerSnapshot ו-hydration
בשרת אין window, אין navigator ואין הרשמה. הארגומנט השלישי אומר ל-React מה לרנדר שם:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
React משתמשת ב-getServerSnapshot גם ברינדור הראשון בדפדפן כשהיא עושה hydration ל-HTML מהשרת, כך ששניהם תואמים. מיד אחרי ה-hydration היא קוראת את getSnapshot ואם הערך האמיתי שונה, מרנדרת שוב איתו. בלי הארגומנט השלישי, הרינדור בשרת זורק "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." אם גבול <Suspense> יושב מעל הקומפוננטה, השרת שולח את ה-fallback של הגבול הזה והדפדפן מרנדר את התוכן שלו במקום; בלי גבול, הרינדור בשרת נכשל.
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;
}
זה עובד, עם שתי חולשות. הרינדור הראשון תמיד מציג את הניחוש ההתחלתי, והערך האמיתי מגיע רינדור אחד מאוחר יותר, אחרי שהאפקט רץ. ועם רינדור concurrent (בזמן transition, למשל), React יכולה לעצור רינדור באמצע; אם ה-store משתנה בזמן העצירה, קומפוננטות שרונדרו לפני ואחרי יכולות להציג ערכים שונים. חוסר העקביות הזה נקרא tearing. useSyncExternalStore קורא את הערך בזמן הרינדור וגורם ל-React לבצע מחדש את הרינדור באופן סינכרוני אם ה-store השתנה, כך שכל קומפוננטה רואה את אותו ערך.
השתמשו בו כשהנתונים חיים מחוץ ל-React: מודול store משלכם, API של הדפדפן, ספרייה חיצונית. רוב ספריות ה-state (Redux, Zustand ואחרות) קוראות לו בשבילכם בתוך ההוקים שלהן. לנתונים ששייכים לקומפוננטות שלכם, useState, useReducer וקונטקסט נשארים הכלים הנכונים.
שאלות נפוצות
בשביל מה useSyncExternalStore?
לקריאת נתונים ש-React לא מחזיקה ושיכולים להשתנות בעצמם: store שנכתב מחוץ ל-React, ספריית state חיצונית, או ערך של הדפדפן כמו navigator.onLine או רוחב החלון. הקומפוננטה מתרנדרת שוב בכל פעם שה-store אומר ל-React שהוא השתנה.
מה subscribe ו-getSnapshot עושים?
subscribe(callback) מתחיל להאזין ל-store, קורא ל-callback בכל שינוי, ומחזיר פונקציה שמפסיקה להאזין. getSnapshot() מחזיר את הערך הנוכחי. React קוראת ל-getSnapshot בזמן הרינדור ואחרי כל התראה, ומרנדרת שוב רק אם הערך השתנה לפי Object.is.
למה getSnapshot צריך להחזיר ערך שמור ב-cache?
React משווה את התוצאה של כל קריאה ל-getSnapshot לקודמת. אם הוא מחזיר אובייקט או מערך חדש בכל פעם, React תמיד רואה שינוי, מרנדרת שוב, קוראת שוב ל-getSnapshot, ונכנסת ללולאה עד שהיא זורקת "Maximum update depth exceeded". החזירו את אותה הפניה עד שהנתונים באמת משתנים.
מה זה getServerSnapshot?
הארגומנט השלישי האופציונלי. הוא מחזיר את הערך לשימוש בזמן רינדור בצד השרת ובזמן hydration בדפדפן, כך ששניהם מייצרים את אותו HTML. בלעדיו, הקומפוננטה זורקת "Missing getServerSnapshot" בשרת, והתוכן שמתחת לגבול ה-<Suspense> הקרוב מרונדר במקום זאת בדפדפן.
להשתמש ב-useSyncExternalStore או ב-useEffect עם useState?
להרשמה לנתונים מבחוץ, העדיפו useSyncExternalStore. הוא קורא את הערך בזמן הרינדור, כך שהרינדור הראשון כבר נכון וכל קומפוננטה רואה את אותו ערך גם בזמן רינדור concurrent. useEffect עם useState מרנדר פעם אחת עם ערך ישן ויכול להציג לרגע ערכים שונים בקומפוננטות שונות.