وقتی هوکهای ریاکت کم میارن؛ کوچ به Zustand
خلاصهٔ کاملتر
نویسنده میگه تو کارهای ریاکتیش اول از همه سراغ هوک میره، چون سادهترین راه برای نگه داشتن state نزدیک UIه. برای یه قابلیت تشخیص گفتار هم همین کارو کرد: یه هوک سفارشی که transcript رو بهصورت زنده استریم میکرد، وضعیت «کاربر داره حرف میزنه» رو نگه میداشت و خطاها رو به UI میرسوند. اولش عالی جواب میداد، تا وقتی که نیازمندیها عوض شدن و همون transcript باید تو چند جای مختلف اپ دیده میشد — اونم با آپدیتهایی که بعضیاشون از سرویسهای بیرون درخت کامپوننتها میاومدن.
زنگ خطر اول یه مسئلهٔ معماری نبود، یه باگ بود: کاربرا گاهی transcript بیات میدیدن، با اینکه رویدادهای تشخیص گفتار همچنان میرسیدن. نویسنده میگه اول مرورگر رو مقصر دونست، بعد ترتیب رویدادها، و بعد چند ساعت وقت گذاشت تا آپدیتهای state رو بین افکتها، رفها، کالبکها و رندرهای دوباره ردیابی کنه. مشکل واقعی این بود که چند کامپوننت به یه state واحد وابسته بودن، ولی مالکیت اون state هنوز داخل هوکی دفن شده بود که برای یه جریان UI طراحی شده بود.
به بیان خودش، از نظر فنی چیزی خراب نبود؛ فقط طراحی دیگه با نحوهٔ استفادهٔ واقعی از فیچر جور درنمیاومد. هوک کمکم مسئول همهچی شده بود: چرخهٔ عمر تشخیص گفتار، رویدادهای خاص مرورگر، آپدیت transcript، وضعیت حرف زدن، گزارش خطا و همگامسازی داده بین چند مصرفکننده. API بیرونیش هنوز ساده به نظر میرسید، ولی پیادهسازیش دیگه ساده نبود.
چرا Context کافی نبود؟ چون مسئله فقط بهاشتراکگذاری state نبود. آپدیتها باید از بیرون کامپوننتهای ریاکت هم میاومدن (رویدادهای مرورگر بهصورت ناهمگام میرسن)، ضمن اینکه بعضی مصرفکنندهها فقط به transcript کار داشتن و بعضیا فقط به وضعیت حرف زدن. نویسنده میخواست آپدیت state مستقل از مالکیت کامپوننت اتفاق بیفته — و همین هلش داد سمت یه استور اختصاصی.
راهحل، بردن state به Zustand بود:
const useRecognitionStore = create((set) => ({
transcript: "",
isUserSpeaking: false,
setTranscript: (transcript) => set({ transcript }),
setSpeaking: (isUserSpeaking) => set({ isUserSpeaking })
}));حالا سرویس تشخیص گفتار مستقیم و بدون اینکه کاری به ریاکت داشته باشه، استور رو آپدیت میکنه:
recognition.onresult = (event) => {
useRecognitionStore.getState().setTranscript(event.results[0][0].transcript);
};سمت UI هم هر کامپوننت با یه سلکتور فقط همون تیکهای رو که لازم داره سابسکرایب میکنه (مثلاً فقط state.transcript)، پس رندر اضافی هم نمیگیره.
به گفتهٔ نویسنده، بزرگترین دستاورد این مهاجرت پرفورمنس نبود؛ از بین رفتن ابهام دربارهٔ مالکیت state بود. قبلش مدام باید به این سؤالا جواب میداد که «این state مال کدوم کامپوننته؟»، «چرا این آپدیت اینجا دیده نمیشه؟» و «کدوم افکت این مقدار رو عوض میکنه؟». بعدش جریان کار ساده شد: رویدادها استور رو آپدیت میکنن، کامپوننتها از استور میخونن — یه منبع حقیقت واحد بهجای stateای که ته پیادهسازی یه هوک قایم شده.
نویسنده تأکید میکنه هنوزم زیاد از هوک استفاده میکنه: اگه state متعلق به خود کامپوننته، useState معمولاً جواب درسته و برای کپسوله کردن رفتار قابلاستفادهٔ مجدد UI هم هوک سفارشی دقیقاً همون چیزیه که میخوای. ولی به محض اینکه state بین بخشهای بیربط اپ مشترک شد و از سیستمهای بیرونی آپدیت گرفت، اون رو دیگه state سطح اپلیکیشن حساب میکنه، نه state کامپوننت. حرف آخرش هم قشنگه: انتقال به Zustand پیچیدگی رو کم نکرد — پیچیدگی از قبل وجود داشت — فقط اون رو دیدنی کرد و یه جای درست براش گذاشت.
نکات کلیدی:
- هوک برای state متعلق به یه کامپوننت یا رفتار قابلاستفادهٔ مجدد عالیه، ولی برای state مشترکِ کل اپ نه.
- علامت هشدار: باگهای دادهٔ بیات و ساعتها ردیابی بین افکتها، رفها و رندرها.
- Context مشکل رو حل نمیکرد، چون آپدیتها باید از بیرون درخت ریاکت هم میاومدن.
- با Zustand، سرویسهای بیرونی با getState() مستقیم استور رو آپدیت میکنن.
- کامپوننتها با سلکتور فقط همون بخشی رو میخونن که لازم دارن.
- سود اصلی: شفاف شدن مالکیت state و داشتن یه منبع حقیقت واحد، نه سرعت بیشتر.




