useSyncExternalStore מאפשר לרכיב לקרוא נתונים שנמצאים מחוץ ל-React מבלי להסתכן בתקלות ויזואליות שרינדור מקבילי (concurrent rendering) עלול להציג. הוא מחליף את התבנית הישנה של משיכת ערכים חיצוניים לתוך state מקומי באמצעות useEffect, תבנית שעלולה לגרום ל-"tearing" – מצב שבו חלקים מה-UI מציגים ערכים שונים במהלך אותה רינדור.

למה התבנית הישנה נשברת תחת concurrency

לפני React 18, המתכון הנפוץ היה:

const [value, setValue] = useState(initial);
useEffect(() => {
  const unsubscribe = externalSource.subscribe(v => setValue(v));
  return unsubscribe;
}, []);

ה-effect רץ לאחר שהרכיב עבר רינדור. כאשר ה-concurrent renderer של React מחליט להשהות, לבטל או להריץ מחדש רינדור, ה-effect עשוי לפעול בנקודה שונה מזו שבה ה-UI קורא את ה-state. התוצאה היא חוסר התאמה: רכיב אחד עשוי לקרוא את הערך הישן, רכיב אחר את החדש, והמסך מציג לזמן קצר נתונים לא עקביים. ה-"tearing" הזה אינו מזיק בעולם סינכרוני טהור, אך הוא הופך לבאג ויזואלי כאשר React מרנדר מספר גרסאות של ה-UI במקביל.

איך useSyncExternalStore פותר את הבעיה

ה-hook דורש שני callbacks:

  • subscribe – רושם listener ומחזיר פונקציה שמסירה אותו.
  • getSnapshot – מחזיר את הערך הנוכחי של המקור החיצוני.

React קורא ל-subscribe כאשר הרכיב עובר mount ומבטל אותו ב-unmount. במהלך כל רינדור, הוא מפעיל את getSnapshot ומבטיח שכל הרכיבים המשתמשים באותו external store יקראו בדיוק את אותו snapshot. מכיוון שהקריאה מתבצעת בתוך שלב הרינדור (render phase), ה-renderer יכול לתאם עדכונים לאורך כל העץ, ובכך למנוע tearing.

ספריות כמו Zustand ו-Redux כבר עוטפות את ה-stores שלהן ב-hook זה, ולכן הן עובדות "מהקופסה" עם התכונות המקביליות (concurrent features) של React 18.

כללים למניעת התנהגות לא תקינה של ה-hook

  1. החזר רק את מה שהרכיב צריך – אובייקט גדול מאלץ את React להשוות מבנים עמוקים בכל עדכון. snapshot צר ושמר יותר שומר על זיהוי שינויים זול ומהיר.
  2. שמור על זהות רפרנציאלית (referential identity) – אם הערך החיצוני לא השתנה, getSnapshot חייב להחזיר את אותה הפניה לאובייקט (object reference). החזרת אובייקט חדש בכל קריאה תגרום ללולאת רינדור אינסופית כי React יזהה שינוי בכל רינדור.
  3. הגבל את השימוש למקורות חיצוניים בלבד – state מקומי של רכיב שייך ל-useState או useReducer. שימוש ב-hook של external-store עבור ערכים פנימיים בלבד מוסיף עקיפות (indirection) מיותרת.

מתי כדאי להשתמש ב-useSyncExternalStore

  • עטיפת Browser APIs (גודל חלון, סטטוס רשת, media queries).
  • חיבור JavaScript store פשוט שאינו מכיר את React.
  • החלפת prop-drilling במקור קריאה גלובלי ולקריאה בלבד (read-only) שאליו נרשמים רכיבים רבים.

הטיעון הנגדי

התבנית של useState + useEffect עדיין עובדת עבור נתונים פשוטים שמשתנים לעיתים רחוקות, במיוחד כאשר האפליקציה אינה מפעילה תכונות concurrent. זוהי גישה ברמה נמוכה יותר שחלק מהמפתחים מוצאים קלה יותר להבנה, מכיוון שלוגיקת הרישום (subscription) נמצאת במפורש בתוך ה-effect. עם זאת, המחיר הוא הסיכון ל-tearing ברגע שמפעילים רינדור מקבילי. עבור פרויקטים שכבר משתמשים ביכולות החדשות של React 18, הבטיחות הנוספת של useSyncExternalStore עולה על ה-boilerplate המינימלי שנוסף.

מה כדאי לעקוב אחריו בהמשך

  • ודאו ש-getSnapshot של ה-store שלכם מחזיר הפניה יציבה; console.log מהיר של ההפניה במהלך הפיתוח יכול לחשוף יצירה מקרית של אובייקטים חדשים.
  • שמרו על פונקציית ה-subscription קלה – עבודה כבדה צריכה להתבצע בתוך המקור החיצוני, ולא בתוך ה-callback המועבר ל-React.
  • בדקו רכיבים תחת ה-concurrent mode של React (למשל, באמצעות act עם ReactDOM.createRoot) כדי לוודא שלא מופיע tearing.

הדגמה חיה מראה את ה-hook בפעולה, מסנכרנת מונה (counter) פשוט המאוחסן מחוץ ל-React ומעדכנת מספר רכיבים ללא תקלות ויזואליות. הדגמה זמינה בכתובת https://usesyncexternalstore.vercel.app/. קוד המקור נמצא ב-GitHub, ומאמר ארוך יותר מסביר את פרטי המימוש.

בשורה התחתונה: בעולם שבו React יכול לרנדר חלקים מה-UI מחוץ לסדר, useSyncExternalStore הוא הגשר האמין בין נתונים חיצוניים לרינדור רכיבים. הוא שומר על עקביות ויזואלית, מתרחב ל-global stores, וכבר מוטמע בספריות ניהול state פופולריות. אמצו אותו בכל מקום שבו עליכם לקרוא state שאינו שייך ל-React, ושמרו על snapshots קטנים ויציבים כדי להפיק ממנו את מלוא התועלת.