فرانتاند رو مثل یه سیستم داده ببین
خلاصهٔ کاملتر
نویسنده برای توضیح فرانتاند به مهندسهای سیستم، یه اتاق تماشای مشترک میسازه: یه پلیر یوتیوب با یه پلیلیست کنارش. آلیس از گوشیش یه ویدیو انتخاب میکنه و لپتاپ تو باید همون سطر رو هایلایت کنه، عنوانش رو نشون بده و ویدیو رو لود کنه. برای هر داده سه چیز رو مشخص میکنه: مالکش کیه، چه ویوهایی ازش مشتق میشن، و آپدیت باید از چه مرزی رد بشه.
ریاکت اول درخت کامپوننتها رو رندر میکنه، یعنی خروجی رو حساب میکنه، بعد تغییرات لازم رو یهجا روی DOM کامیت میکنه. نویسنده میگه DOM رو مثل یه materialized view ببین، یعنی همون ویوی از پیش محاسبهشدهای که تو دیتابیسها داری: کامپوننتها فرمول مشتقگیری رو تعریف میکنن و ریاکت ویو رو با تغییر ورودیها بهروز نگه میداره. چون کامیت بدون وقفه انجام میشه، مرورگر وسط کار صفحه رو دوباره نقاشی نمیکنه و عنوان و هایلایت با هم ظاهر میشن.
انتخاب ویدیو و باز بودن سایدبار مالکشون خود ریاکته و با useState نگه داشته میشن. اشتباه رایجی که نویسنده روش دست میذاره اینه که یه مقدار مشتقشده از props رو تو state کپی کنی و با یه Effect هماهنگش کنی. اونوقت یه آپدیت منطقی بین دو تا کامیت تیکه میشه: کامیت اول هایلایت ویدیوی جدید رو نشون میده ولی عنوان هنوز مال قبلیه و همین حالت ناسازگار میتونه یه پرش لحظهای بسازه. راه درست اینه که عنوان همون موقع رندر از ویدیوی انتخابشده مشتق بشه.
ولی پلیر جاسازیشده فرق داره. انتخاب ما فقط میگه چه ویدیویی رو میخوایم؛ خود پلیر مالک چیزیه که واقعاً لود شده و باید بهش دستور داد. صدا زدن پلیر وسط رندر خطرناکه، چون ریاکت ممکنه اون رندر نیمهتموم رو دور بندازه و پلیر ویدیویی رو لود کنه که هیچوقت تو UI نیومده. برای همین این کار با useEffect بعد از کامیت انجام میشه:
useEffect(() => {
player.loadVideoById(selectedId)
}, [player, selectedId])لیست وابستگیها تعیین میکنه کِی دوباره اجرا بشه، پس بستن سایدبار که انتخاب رو عوض نمیکنه، پلیر رو بیخودی دوباره لود نمیکنه.
قدم بعدی اینه که انتخاب بعد از رفرش هم بمونه، پس مالکیتش از ریاکت میره به IndexedDB و ریاکت فقط خوانندهی یه state بیرونی میشه. اگه بخوای با useState و یه Effect وصلش کنی دو تا مشکل داری: بین خوندن اولیه و ثبت اشتراک یه شکاف هست که آپدیت توش گم میشه، و بدتر اینکه اگه دو کامپوننت جدا از هم از استور بخونن و ریاکت وسطش مکث کنه، یکی نسخهی قدیمی و اون یکی نسخهی جدید رو میبینه. اسم این حالت tearing ـه، یعنی پاره شدن ویو بین دو نسخه از یه منبع.
const { selectedId } = useSyncExternalStore(
subscribe,
getSnapshot,
)useSyncExternalStore همین هماهنگی رو انجام میده: خودش اشتراک رو مدیریت میکنه و قبل از کامیت اسنپشاتها رو دوباره میخونه و اگه عوض شده باشن رندر رو تکرار میکنه، دقیقاً مثل کنترل همزمانی خوشبینانه تو دیتابیس. شرطش اینه که getSnapshot یه مقدار تغییرناپذیر برگردونه و تا وقتی داده عوض نشده همون مقدار رو بده، چون ریاکت با Object.is مقایسه میکنه.
مرحلهی آخر اشتراک انتخاب بین دستگاههاست: حالا بکاند مالک انتخاب اتاقه و هر کلاینت یه replica داره، ولی اتصال استور به ریاکت همون قبلیه. نویسنده میگه کتابخونهها این مسئولیتها رو جورواجور بستهبندی میکنن؛ Electric تغییرات رکوردهای Postgres رو میرسونه، Convex نتیجهی کوئریها رو روی کلاینت تازه نگه میداره، LiveStore رویدادها رو سینک میکنه و TanStack DB با همون useSyncExternalStore نتیجهی کوئریها رو به رندر وصل میکنه. باز بودن سایدبار هم محلی میمونه، چون باز کردن سایدبار آلیس نباید سایدبار باب رو باز کنه.
نکات کلیدی:
- برای هر داده سه چیز رو مشخص کن: مالک، ویوهای مشتقشده، و مرزی که آپدیت باید ازش رد بشه.
- مقداری که از props مشتق میشه رو تو state کپی نکن؛ این کار یه آپدیت رو به دو کامیت تقسیم میکنه و پرش تصویری میسازه.
- useEffect برای همگامسازی با چیزهای بیرون ریاکت مثل پلیر یوتیوبه، نه برای نگه داشتن مقدارهای مشتقشده.
- useSyncExternalStore جلوی tearing رو میگیره؛ getSnapshot باید مقدار تغییرناپذیر و پایدار برگردونه چون مقایسه با Object.is انجام میشه.
- Electric و Convex و LiveStore مرز بکاند تا استور کلاینت رو پوشش میدن و TanStack DB مرز استور تا ریاکت رو.




