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 اضافی را به پروژه تحمیل می‌کند.