useSyncExternalStore به یک کامپوننت اجازه می‌دهد داده‌هایی را که خارج از React قرار دارند بخواند، بدون اینکه با خطر بروز ناهماهنگی‌های بصری (visual glitches) ناشی از رندرینگ همزمان (concurrent rendering) روبرو شود. این هوک جایگزین الگوی قدیمیِ کشیدن مقادیر خارجی به حالت محلی (local 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 در نقطه‌ای متفاوت از رابط کاربری که حالت (state) را می‌خواند، اجرا شود. نتیجه یک عدم تطابق است: یک کامپوننت ممکن است مقدار قدیمی را خوانده باشد، دیگری مقدار جدید را، و صفحه نمایش برای لحظه‌ای داده‌های ناسازگاری را نشان دهد. این «پارگی» (tearing) در یک دنیای کاملاً همگام (synchronous) بی‌خطر است، اما زمانی که React چندین نسخه از رابط کاربری را به صورت موازی رندر می‌کند، به یک باگ قابل مشاهده تبدیل می‌شود.

چگونه useSyncExternalStore مشکل را حل می‌کند

این هوک به دو تابع بازگشتی (callback) نیاز دارد:

  • subscribe – یک شنونده (listener) را ثبت می‌کند و تابعی را برمی‌گرداند که آن را حذف می‌کند.
  • getSnapshot – مقدار فعلی منبع خارجی را برمی‌گرداند.

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

کتابخانه‌هایی مانند Zustand و Redux از قبل استورهای خود را با این هوک پوشش داده‌اند، به همین دلیل است که آن‌ها به صورت پیش‌فرض با ویژگی‌های همزمانی (concurrent features) در React 18 سازگار هستند.

قوانینی برای جلوگیری از عملکرد نادرست هوک

  1. فقط آنچه کامپوننت نیاز دارد را برگردانید – یک شیء بزرگ، React را مجبور می‌کند در هر به‌روزرسانی، ساختارهای عمیق را مقایسه کند. یک اسنپ‌شات محدود و کوچک، تشخیص تغییرات را ارزان و سریع نگه می‌دارد.
  2. هویت مرجع (referential identity) را حفظ کنید – اگر مقدار خارجی تغییر نکرده است، getSnapshot باید همان مرجع شیء (object reference) را برگرداند. بازگرداندن یک شیء جدید در هر فراخوانی، باعث ایجاد یک حلقه رندر بی‌نهایت می‌شود، زیرا React در هر رندر یک تغییر مشاهده می‌کند.
  3. استفاده را به منابع خارجی محدود کنید – حالت محلی کامپوننت باید در useState یا useReducer قرار بگیرد. استفاده از هوک استور خارجی برای مقادیر صرفاً داخلی، باعث پیچیدگی غیرضروری می‌شود.

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

  • پوشش دادن APIهای مرورگر (اندازه پنجره، وضعیت آنلاین بودن شبکه، media queries).
  • اتصال یک استور جاوااسکریپت ساده که از React بی‌اطلاع است.
  • جایگزینی prop-drilling با یک منبع خواندنی (read-only) سراسری که بسیاری از کامپوننت‌ها در آن مشترک هستند (subscribe می‌کنند).

استدلال مخالف

الگوی useState + useEffect هنوز برای داده‌های ساده و با تغییرات کم کار می‌کند، به‌ویژه زمانی که یک اپلیکیشن ویژگی‌های همزمانی را فعال نکرده باشد. این یک رویکرد سطح پایین‌تر است که برخی از توسعه‌دهندگان درک آن را آسان‌تر می‌دانند، زیرا منطق اشتراک (subscription logic) به طور صریح در effect قرار دارد. با این حال، هزینه این کار، خطر بروز پارگی (tearing) پس از فعال شدن رندرینگ همزمان است. برای پروژه‌هایی که از قابلیت‌های جدید React 18 استفاده می‌کنند، امنیت اضافی useSyncExternalStore بر پیچیدگی اندک کدنویسی (boilerplate) اضافه شده، برتری دارد.

نکات بعدی برای بررسی

  • بررسی کنید که getSnapshot استور شما یک مرجع پایدار (stable reference) برمی‌گرداند؛ یک console.log سریع از مرجع در طول توسعه می‌تواند بازسازی تصادفی اشیاء را آشکار کند.
  • تابع اشتراک (subscription function) را سبک نگه دارید – کارهای سنگین باید در داخل منبع خارجی انجام شود، نه در تابع بازگشتی که به React پاس داده می‌شود.
  • کامپوننت‌ها را در حالت همزمان React تست کنید (مثلاً با استفاده از act همراه با ReactDOM.createRoot) تا مطمئن شوید هیچ پارگی‌ای رخ نمی‌دهد.

یک دمو زنده، عملکرد این هوک را در عمل نشان می‌دهد: همگام‌سازی یک شمارنده ساده که خارج از React ذخیره شده و به‌روزرسانی چندین کامپوننت بدون ناهماهنگی‌های بصری. این دمو در https://usesyncexternalstore.vercel.app/ در دسترس است. کد منبع در GitHub قرار دارد و یک نوشتار طولانی‌تر جزئیات پیاده‌سازی را توضیح می‌دهد.

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