React 18 هوک useSyncExternalStore را اضافه کرد؛ هوکی که به کامپوننت‌ها اجازه می‌دهد داده‌ها را از هر منبعی که خارج از درخت React قرار دارد — مانند APIهای مرورگر، WebSocketها یا استورهای شخص ثالث — بدون ناهماهنگی‌های بصری (visual glitches) بخوانند. این ناهماهنگی‌ها پیش‌تر در الگوی قدیمیِ بازتاب دادن (mirroring) آن داده‌ها با استفاده از useState و useEffect وجود داشت. اگر تا به حال شاهد پرش (flicker) در رابط کاربری بوده‌اید — مثلاً زمانی که بخشی از صفحه اندازه جدید پنجره را نشان می‌داد اما بخش دیگر هنوز اندازه قدیمی را نمایش می‌داد — این هوک همان راه حلی است که منتظرش بودید.

چرا وضعیت (state) خارجی به یک هوک جدید نیاز دارد

کامپوننت‌های React همیشه قادر بوده‌اند به چیزهایی که فراتر از محدوده خودشان وجود دارند مشترک (subscribe) شوند: شیء window، یک استور Redux یا یک اتصال WebSocket. رویکرد مرسوم این بود که یک اشتراک در داخل یک useEffect راه‌اندازی شود، مقادیر ورودی از طریق setState به وضعیت محلی منتقل شود و اجازه داده شود تا React دوباره رندر شود. این روش زمانی که React به صورت همزمان (synchronously) رندر می‌کند، به خوبی کار می‌کند، اما React 18 رندرینگ همزمان (concurrent rendering) را معرفی کرد که در آن ممکن است یک کامپوننت چندین بار قبل از اینکه مرورگر واقعاً آن را ترسیم (paint) کند، رندر شود. در این حالت، الگوی «انتقال به وضعیت محلی» می‌تواند باعث بروز tearing شود — یعنی بخش‌های مختلف رابط کاربری در طول یک چرخه رندر واحد، اسنپ‌شات‌های متفاوتی از یک مقدار خارجی را بخوانند.

useSyncExternalStore برای جلوگیری از tearing ساخته شده است. این هوک از منبع خارجی یک snapshot (مقدار فعلی) و یک تابع subscribe (نحوه اطلاع‌رسانی از تغییرات) می‌خواهد. React هنگام مانت (mount) شدن کامپوننت، تابع subscribe را فراخوانی کرده و هنگام آن‌مانت (unmount) شدن، به طور خودکار آن را لغو می‌کند. هر زمان که اسنپ‌شات بازگشتی از getSnapshot تغییر کند، React یک رندر را برنامه‌ریزی می‌کند که برای کل درخت کامپوننت، همان اسنپ‌شات را می‌خواند و بدین ترتیب سازگاری داده‌ها تضمین می‌شود.

دو تابع مورد نیاز

تابع عملکرد
subscribe یک callback دریافت می‌کند که باید هر زمان مقدار خارجی تغییر کرد، فراخوانی شود. این تابع یک تابع unsubscribe برمی‌گرداند که React هنگام پاکسازی (cleanup) آن را فراخوانی می‌کند.
getSnapshot مقدار فعلی را از منبع خارجی برمی‌گرداند. این تابع باید از نظر مرجع پایدار (referentially stable) باشد — یعنی در هر رندر همان شیء تابع باشد — تا React بتواند اسنپ‌شات‌ها را به درستی مقایسه کند.

یک پیاده‌سازی حداقلی برای وضعیت 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 تنها زمانی آن را دوباره رندر می‌کند که navigator.onLine واقعاً تغییر کند، و هر رندر همان مقدار را مشاهده خواهد کرد.

قوانینی برای جلوگیری از بروز خطا در هوک

  1. کوچک‌ترین مقدار داده‌ای که کامپوننت به آن نیاز دارد را برگردانید.
    اگر استور خارجی یک شیء بزرگ را نگه می‌دارد اما کامپوننت فقط به یک فیلد اهمیت دارد، فقط همان فیلد را برگردانید. بازگرداندن مقادیر بزرگ‌تر باعث رندرهای غیرضروری می‌شود.

  2. تابع getSnapshot را مموئیز (Memoise) کنید.
    اگر تابع را در هر رندر دوباره ایجاد کنید، React با آن به عنوان یک منبع جدید برخورد کرده و ممکن است در یک حلقه بی‌نهایت قرار بگیرد. آن را در useCallback قرار دهید یا خارج از کامپوننت تعریف کنید.

  3. از بازگرداندن اشیاء جدید در هر فراخوانی خودداری کنید.
    بازگرداندن یک شیء جدید (مثلاً { count: store.getCount() }) در هر رندر یک مرجع جدید ایجاد می‌کند و باعث می‌شود React تصور کند اسنپ‌شات تغییر کرده و یک حلقه رندر بی‌نهایت ایجاد شود. از مقادیر اولیه (primitives)، رشته‌ها، اعداد یا اشیاء مموئیز شده استفاده کنید.

  4. از آن برای وضعیت داخلی کامپوننت استفاده نکنید.
    useState همچنان ابزار مناسب برای داده‌هایی است که فقط داخل کامپوننت قرار دارند. useSyncExternalStore سربار اضافی ایجاد می‌کند که برای وضعیت محلی (local state) نیازی به آن نیست.

چه زمانی از آن استفاده کنیم

  • Browser APIs – اندازه پنجره (window size)، کوئری‌های رسانه‌ای (media queries)، navigator.onLine و وضعیت باتری.
  • Framework-agnostic stores – استورهای مستقل از فریم‌ورک مانند Zustand، Redux، MobX یا هر استور سفارشی که API اشتراک (subscription) را ارائه می‌دهد.
  • منابع تغییرپذیر که خارج از React به‌روز می‌شوند – پیام‌های WebSocket، رویدادهای تغییر IndexedDB و اعلان‌های Service Worker.

اگر منبع داده شما از قبل داخل React قرار دارد (مثلاً وضعیت یک کامپوننت والد)، از useState یا context استفاده کنید.

آنچه جامعه برنامه‌نویسان می‌گویند

کاربران اولیه گزارش می‌دهند که useSyncExternalStore پرش‌هایی (flicker) را که هنگام تغییر اندازه پنجره در تنظیمات رندرینگ همزمان (concurrent-rendering) مشاهده می‌شد، از بین می‌برد. برخی کتابخانه‌ها از قبل هوک‌های داخلی خود را به این API تغییر داده‌اند و رفتار قابل پیش‌بینی‌تری را در نسخه‌های مختلف React وعده می‌دهند. هزینه این کار، بار ذهنی کمی بالاتر است: توسعه‌دهندگان باید به پایداری (stability) این دو تابع فکر کنند و از بازگرداندن اشیاء جدید در هر فراخوانی خودداری کنند.

موضوعات بعدی برای دنبال کردن

نسخه‌های آتی React ممکن است قواعد مربوط به useSyncExternalStore را سخت‌گیرانه‌تر کنند و احتمالاً کمک‌کننده‌های (helpers) داخلی برای APIهای رایج مرورگر اضافه کنند. برای اطلاع از هرگونه منسوخ شدن (deprecation) الگوی قدیمی که مشابه useEffect بود، وبلاگ رسمی React را دنبال کنید. در این میان، این هوک پایدار و بخشی از API عمومی است، بنابراین می‌توانید با خیال راحت کدهای مربوط به وضعیت خارجی (external-state) موجود را بازنویسی (refactor) کنید.

تست سریع

یک دموی زنده که همگام‌سازی اندازه پنجره (window-size syncing)، تشخیص آنلاین/آفلاین بودن و یک استور ساده Zustand را نمایش می‌دهد، در https://usesyncexternalstore.vercel.app/ در دسترس است. کد منبع، همراه با توضیحات کامل، در https://github.com/dev48v/usesyncexternalstore موجود است.

نکته کلیدی: useSyncExternalStore یک پل قابل اعتماد برای React به هرگونه داده تغییرپذیر (mutable) که خارج از چرخه رندر آن قرار دارد فراهم می‌کند و از بروز پدیده tearing و رندرهای غیرضروری جلوگیری می‌کند. آن را برای منابع خارجی به کار بگیرید، اسنپ‌شات (snapshot) را حداقلی و پایدار نگه دارید و اجازه دهید کار اصلی حفظ یکپارچگی را React انجام دهد.