قبل از اینکه Store جدیدی به پروژه اضافه کنم، اول مشخص می کنم این State واقعاً متعلق به کجاست، چند مصرف کننده دارد و چه مدت باید زنده بماند.
این سه سؤال معمولاً مهم تر از انتخاب بین Zustand، Context یا هر Library دیگری هستند. اگر مالکیت State اشتباه تعریف شود، ابزار قوی تر فقط همان اشتباه را در سطح بزرگ تری پخش می کند.
برای من State Management از انتخاب Library شروع نمی شود؛ از مدل کردن داده شروع می شود.
اول State را بر اساس منبع حقیقت دسته بندی می کنم
همه Stateها یک جنس نیستند. باز بودن Modal با داده Dashboard، Filter داخل URL و Cart کاربر چرخه عمر یکسانی ندارند.
| نوع State | نمونه | محل معمول |
|---|---|---|
| Local UI | Modal، Accordion، Hover، Draft کوچک | نزدیک Component |
| URL State | Search، Filter، Sort، Pagination | Search Params / Route |
| Server State | Products، Orders، Dashboard Data | Data-fetching / Cache Layer |
| Shared Client State | Selection مشترک، UI Preference، Cart موقت | Context یا Store |
| Form State | فرم ساده یا چندمرحله ای | Component، Form Layer یا Store محدود |
این جدول قانون قطعی نیست، اما جلوی یک عادت بد را می گیرد: انتقال هر داده ای که دو Component می خواهند به Global Store.
پنج سؤال قبل از ساخت Store
1. منبع حقیقت کجاست؟
اگر داده از API می آید، Server صاحب اصلی آن است. کپی کردن Response داخل Store و نگهداری دستی Loading، Error، Refresh و Invalidation معمولاً دو Source of Truth می سازد.
2. چند بخش واقعاً باید آن را تغییر دهند؟
اگر فقط یک Component و Childهای نزدیکش State را مصرف می کنند، Local State یا Lifting State Up معمولاً کافی است.
3. آیا State با Navigation باید بماند یا از بین برود؟
باز بودن Dropdown بعد از تغییر Route معمولاً مهم نیست؛ Filter محصول شاید مهم باشد؛ Cart قطعاً عمر متفاوتی دارد.
4. آیا کاربر باید بتواند این وضعیت را Share یا Bookmark کند؟
اگر Filter، Sort، Search یا Pagination روی چیزی که کاربر می بیند اثر مهم دارد، URL اغلب مکان بهتری از Store است. Back/Forward مرورگر هم بدون کد اضافه درست کار می کند.
5. آیا Persistence لازم است؟
موضوع Persistence یک Requirement جداست، نه دلیل Global کردن State. localStorage، Cookie، Session یا Server هرکدام رفتار و ریسک خودشان را دارند.
برای Modal معمولاً Store لازم نیست
فرض کنید یک صفحه Product یک Modal راهنمای سایز دارد. اگر فقط همان صفحه آن را باز و بسته می کند، این State به سادگی می تواند Local بماند:
const [isSizeGuideOpen, setSizeGuideOpen] = useState(false);
بردن این مقدار به Store فقط یک API جدید برای چیزی می سازد که Owner آن کاملاً مشخص است.
اما اگر یک Command Palette یا Authentication Modal از Header، Pricing Card و Checkout Trigger می شود، Shared Client State می تواند منطقی باشد؛ چون چند Consumer دور از هم باید روی یک رفتار مشترک هماهنگ شوند.
تفاوت در «Modal بودن» نیست؛ در مالکیت و دامنه مصرف است.
برای Filter قابل Share معمولاً URL State مناسب است
برای صفحه محصولات، Filterها خیلی سریع به Store منتقل می شوند. من قبل از آن می پرسم آیا این وضعیت باید با Refresh باقی بماند و آیا URL باید همان View را بازسازی کند.
اگر جواب مثبت است، Search Params انتخاب طبیعی تری هستند:
/products?category=shoes&sort=price-desc&page=2
مزیت فقط SEO نیست. کاربر لینک را Copy می کند، Back/Forward درست کار می کند و State با Router هم راستا می ماند.
استفاده از Store هنوز ممکن است برای UI موقتی Filter Drawer لازم باشد، اما Query انتخاب شده لازم نیست هم زمان در URL و Store کپی شود.
وضعیت Server State را با Client State یکی نمی کنم
داده سفارش ها، موجودی محصول یا Dashboard از Server می آید و ممکن است Stale شود. این داده نیاز به Fetch، Cache، Retry، Refetch و Invalidation دارد.
اگر آن را مثل یک Object معمولی در Store نگه دارم، مسئولیت Data-fetching Layer را دوباره داخل Store پیاده می کنم.
برای Dashboard معمولاً ترجیح می دهم Query/Loader مسئول Server Data باشد و Component فقط View State خودش را نگه دارد؛ مثلاً Tab فعال یا Range انتخاب شده.
این تفکیک باعث می شود Refresh داده و تعامل UI به هم گره نخورند.
در Cart با یک مثال خاکستری روبه رو هستیم
موضوع Cart همیشه «Client State» نیست.
در فروشگاه Guest ممکن است بخشی از Cart ابتدا در Browser نگه داشته شود. بعد از Login یا در Checkout، Server می تواند Source of Truth باشد. در بعضی پروژه ها هم Cart از ابتدا از API می آید.
پس تصمیم درست به مدل محصول بستگی دارد:
- آیا Cart باید بین Deviceها Sync شود؟
- آیا Price و Stock باید در Server دوباره Validate شوند؟
- Guest Cart چطور Merge می شود؟
- Optimistic Update لازم است؟
یک Store می تواند UI را سریع نگه دارد، اما نباید نسخه Client را حقیقت نهایی Price یا Availability فرض کند.
وضعیت Authentication State را از Session جدا می کنم
یکی از الگوهای آسیب پذیر این است که isAuthenticated = true را در Store ذخیره کنیم و آن را حقیقت امنیتی بدانیم.
بخش Client می تواند یک Snapshot از وضعیت کاربر داشته باشد تا Header یا Menu را Render کند، اما اعتبار Session باید از منبع قابل اعتماد بیاید. Store برای UX مناسب است، نه برای اثبات مجوز دسترسی.
در پروژه ای که Route Protection یا Role دارد، Authorization باید در لایه ای enforce شود که کاربر نتواند با تغییر State مرورگر آن را دور بزند.
چه زمانی Context کافی است؟
استفاده از Context برای اشتراک Dependency یا وضعیت با دامنه روشن مناسب است؛ Theme، Locale، یک Service یا State محدود به یک Subtree نمونه های مناسبی هستند.
اگر Context بزرگ شود و Value آن دائماً تغییر کند، Consumerهای بیشتری به Updateها وابسته می شوند و مرز Featureها مبهم می شود. من معمولاً Contextها را بر اساس مسئولیت Split می کنم، نه اینکه یک AppContext برای همه چیز بسازم.
یک Store وقتی جذاب تر می شود که State واقعاً Shared و Mutable باشد، Consumerها از هم دور باشند و Selectorهای محدود بتوانند هر Component را فقط به بخشی که لازم دارد Subscribe کنند.
زمانی Zustand را انتخاب می کنم که Store کوچک بماند
برای من Zustand راه حذف فکر معماری نیست. زمانی مفید است که State مشترک Client داریم و API Store می تواند کوچک و واضح بماند.
مثلاً برای یک مقایسه محصول:
type CompareState = {
ids: string[];
add: (id: string) => void;
remove: (id: string) => void;
clear: () => void;
};
اگر Store شروع کند به نگهداری Product Response، Auth Session، Toast، Modal، Form Data و Theme در یک Object، مشکل Library نیست؛ Boundaries از بین رفته اند.
انتخاب معماری Multi-step Form به دامنه Flow بستگی دارد
فرم چندمرحله ای مثال خوبی است چون پاسخ واحد ندارد.
اگر همه Stepها زیر یک Parent Render می شوند، Form State می تواند همان جا بماند. اگر Stepها Route جدا دارند یا کاربر باید بعداً برگردد، Persistence و URL وارد تصمیم می شوند. اگر چند بخش مستقل روی Draft مشترک کار می کنند، Store محدود به همان Flow می تواند مناسب باشد.
من قبل از Store مشخص می کنم:
- چه فیلدهایی بین Stepها مشترک اند؟
- Validation در هر Step است یا پایان Flow؟
- Back باید Draft را حفظ کند؟
- Refresh چه رفتاری دارد؟
- Draft روی Server ذخیره می شود یا فقط Browser؟
این سؤال ها Architecture را تعیین می کنند؛ نه محبوبیت Library.
خطاهایی که معمولاً پیچیدگی ایجاد می کنند
استفاده از Store به عنوان سطل عمومی
هر State جدید به Store اضافه می شود چون «از قبل Store داریم». بعد از مدتی هیچ Feature مالک مشخصی ندارد.
کپی کردن Server State
وقتی Response داخل Cache Layer وجود دارد و نسخه دوم آن در Store نگه داشته می شود، Synchronization به یک Bug دائمی تبدیل می شود.
نگهداری هم زمان URL و Store
وقتی Filter هم در Search Params است و هم در Store قرار دارد، باید تصمیم بگیریم کدام یک اول Update می شود و در Refresh چه اتفاقی می افتد.
استفاده بیش از حد از Persistence
هر چیز در localStorage ذخیره می شود. State قدیمی، Migration Schema و Hydration به مسئله ای تبدیل می شوند که Requirement اصلی اصلاً آن را نخواسته بود.
استفاده از Actionهای مبهم
setState({ ... }) از همه جا صدا زده می شود و Business Rule بین Componentها پخش می شود. Action محدود و نام دار معمولاً قابل فهم تر است.
فهرست Checklist تصمیم گیری من
قبل از اضافه کردن Store می پرسم:
- Owner این State کدام Feature یا Source است؟
- Local، URL، Server یا Shared Client State است؟
- چند Consumer آن را می خوانند و چند بخش آن را تغییر می دهند؟
- باید با Route، Refresh یا Device دیگر همگام بماند؟
- آیا داده از قبل در Server Cache وجود دارد؟
- Context کوچک تر مسئله را حل می کند؟
- Store API می تواند محدود و Feature-specific بماند؟
- حذف Store بعداً چقدر سخت خواهد بود؟
جمع بندی
توانایی معماری State برای من یعنی بتوانم State را در کوچک ترین محدوده ای که درست کار می کند نگه دارم.
گاهی جواب useState است، گاهی URL، گاهی Data-fetching Layer، گاهی Context و گاهی Zustand. انتخاب خوب آن ابزاری نیست که بیشترین Capability را دارد؛ ابزاری است که Ownership را واضح تر می کند و کمترین Synchronization اضافی را به پروژه تحمیل می کند.
