SN.
همه نوشته ها
مهندسی۳۱ مرداد ۱۴۰۳۹ دقیقه

قبل از Zustand؛ State در React واقعاً متعلق به کجاست؟

قبل از اضافه کردن Store، State را بر اساس مالکیت، طول عمر و مصرف کننده ها دسته بندی می کنم؛ Local، URL، Server یا Shared Client State.

میز کار توسعه دهنده با کد React روی لپ تاپ

قبل از اینکه Store جدیدی به پروژه اضافه کنم، اول مشخص می کنم این State واقعاً متعلق به کجاست، چند مصرف کننده دارد و چه مدت باید زنده بماند.

این سه سؤال معمولاً مهم تر از انتخاب بین Zustand، Context یا هر Library دیگری هستند. اگر مالکیت State اشتباه تعریف شود، ابزار قوی تر فقط همان اشتباه را در سطح بزرگ تری پخش می کند.

برای من State Management از انتخاب Library شروع نمی شود؛ از مدل کردن داده شروع می شود.

اول State را بر اساس منبع حقیقت دسته بندی می کنم

همه Stateها یک جنس نیستند. باز بودن Modal با داده Dashboard، Filter داخل URL و Cart کاربر چرخه عمر یکسانی ندارند.

نوع Stateنمونهمحل معمول
Local UIModal، Accordion، Hover، Draft کوچکنزدیک Component
URL StateSearch، Filter، Sort، PaginationSearch Params / Route
Server StateProducts، Orders، Dashboard DataData-fetching / Cache Layer
Shared Client StateSelection مشترک، 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 اضافی را به پروژه تحمیل می کند.

منابع رسمی برای بررسی بیشتر