React 18 הוסיפה את useSyncExternalStore, hook המאפשר לרכיבים לקרוא נתונים מכל מקור שחי מחוץ לעץ ה-React—ממשקי API של הדפדפן, WebSockets, או חנויות (stores) של צד שלישי—ללא הבעיות הוויזואליות שפקדו את התבנית הישנה של שיקוף הנתונים הללו באמצעות useState ו-useEffect. אם אי פעם ראיתם ממשק משתמש (UI) מהבהב כי חלק ממנו הציג גודל חלון ישן בעוד שחלק אחר כבר שיקף את החדש, ה-hook הזה הוא הפתרון שחיכיתם לו.

למה מצב (state) חיצוני זקוק ל-hook חדש

רכיבי React תמיד יכלו להירשם (subscribe) לדברים שקיימים מחוץ לטווח שלהם: אובייקט ה-window, חנות Redux, או חיבור WebSocket. הגישה המקובלת הייתה להגדיר הרשמה בתוך useEffect, לדחוף ערכים נכנסים לתוך ה-state המקומי באמצעות setState, ולאפשר ל-React לבצע re-render. זה עובד מצוין כש-React מבצע רינדור סינכרוני, אך React 18 הציגה רינדור מקבילי (concurrent rendering), שבו רכיב עשוי להיות מרונדר מספר פעמים לפני שהדפדפן באמת מצייר (paints) את המסך. במצב זה, תבנית ה-"שיקוף-לתוך-state-מקומי" עלולה ליצור tearing—חלקים שונים של ה-UI קוראים תמונות מצב (snapshots) שונות של אותו ערך חיצוני במהלך מחזור רינדור בודד.

useSyncExternalStore נבנה כדי למנוע tearing. הוא מבקש מהמקור החיצוני snapshot (הערך הנוכחי) ופונקציית subscribe (כיצד לקבל הודעה על שינויים). React קוראת ל-subscribe כאשר הרכיב עולה (mounts) ומבטלת את ההרשמה (unsubscribes) באופן אוטומטי בעת הסרת הרכיב (unmount). בכל פעם שה-snapshot המוחזר על ידי getSnapshot משתנה, React מתזמנת רינדור שקורא את אותו ה-snapshot עבור כל עץ הרכיבים, מה שמבטיח עקביות.

שתי הפונקציות הנדרשות

פונקציה מה היא עושה
subscribe מקבלת callback שחייב להיקרא בכל פעם שהערך החיצוני משתנה. היא מחזירה פונקציית unsubscribe ש-React תקרא לה בזמן הניקוי (cleanup).
getSnapshot מחזירה את הערך הנוכחי מהמקור החיצוני. היא חייבת להיות יציבה מבחינת רפרנס (referentially stable)—אותו אובייקט פונקציה בכל רינדור—כדי ש-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 תבצע לו re-render רק כאשר navigator.onLine באמת משתנה, וכל רינדור יראה את אותו הערך.

כללים שמונעים מה-hook "להתפוצץ"

  1. החזירו את החלק הקטן ביותר של הנתונים שהרכיב זקוק לו.
    אם החנות החיצונית מחזיקה אובייקט גדול אך רכיב מתעניין רק בשדה אחד, החזירו רק את השדה הזה. החזרת נתונים גדולים יותר גורמת לרינדורים מיותרים.

  2. בצעו memoization ל-getSnapshot.
    אם אתם יוצרים מחדש את הפונקציה בכל רינדור, React תתייחס אליה כמקור חדש ועלולה להיכנס ללולאה אינסופית. עטפו אותה ב-useCallback או הגדירו אותה מחוץ לרכיב.

  3. הימנעו מהחזרת אובייקטים חדשים בכל קריאה.
    החזרת אובייקט חדש (למשל, { count: store.getCount() }) יוצרת רפרנס חדש בכל רינדור, מה שגורם ל-React לחשוב שה-snapshot השתנה ומפעיל לולאת רינדור אינסופית. השתמשו בטיפוסים פרימיטיביים (primitives), מחרוזות, מספרים, או אובייקטים שעברו memoization.

  4. אל תשתמשו בו עבור state פנימי של רכיב.
    useState הוא עדיין הכלי הנכון עבור נתונים שחיים רק בתוך הרכיב. useSyncExternalStore מוסיף overhead שאינו נחוץ עבור state מקומי.

מתי כדאי להשתמש בו

  • Browser APIs – גודל חלון, media queries, navigator.onLine, סטטוס סוללה.
  • Framework-agnostic stores – Zustand, Redux, MobX, או כל חנות מותאמת אישית החושפת API של הרשמה (subscription).
  • Mutable sources שמתעדכנים מחוץ ל-React – הודעות WebSocket, אירועי שינוי ב-IndexedDB, התראות של Service Worker.

אם מקור הנתונים שלכם כבר חי בתוך React (למשל, ה-state של רכיב אב), היצמדו ל-useState או ל-context.

מה הקהילה אומרת

מאמצים מוקדמים מדווחים ש-useSyncExternalStore מבטל את ההבהוב שראו בעת שינוי גודל חלונות במצב של רינדור מקבילי. חלק מהספריות כבר עברו לשימוש ב-API הזה עבור ה-hooks הפנימיים שלהן, מה שמבטיח התנהגות צפויה יותר בין גרסאות React שונות. המחיר הוא עומס קוגניטיבי מעט גבוה יותר: מפתחים חייבים לחשוב על היציבות של שתי הפונקציות ולהימנע מהחזרת אובייקטים חדשים בכל קריאה.

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

הגרסאות הקרובות של React עשויות להדק את החוזה סביב useSyncExternalStore, וייתכן שיוסיפו עוזרים (helpers) מובנים עבור ממשקי API נפוצים של הדפדפן. כדאי לעקוב אחר הבלוג הרשמי של React כדי להתעדכן בכל הודעת deprecation לגבי התבנית הישנה של חיקוי useEffect. בינתיים, ה-hook יציב והוא חלק מה-API הציבורי, כך שניתן לבצע refactor בבטחה לקוד קיים של מצב חיצוני (external-state).

בדיקה מהירה

דמו חי המציג סנכרון גודל חלון, זיהוי מקוון/לא מקוון (online/offline), ו-store פשוט של Zustand זמין בכתובת https://usesyncexternalstore.vercel.app/. קוד המקור, הכולל הערות, זמין בכתובת https://github.com/dev48v/usesyncexternalstore.

בשורה התחתונה: useSyncExternalStore מעניק ל-React גשר אמין לכל נתון משתנה (mutable data) שחי מחוץ למחזור הרינדור שלה, ובכך מונע tearing ורינדורים מיותרים. השתמשו בו עבור מקורות חיצוניים, שמרו על ה-snapshot מינימלי ויציב, ותנו ל-React לבצע את העבודה הקשה של שמירה על עקביות.