Actions و State Transitions

در درس قبلی با ساختار کلی Reducer و useReducer آشنا شدیم. در این درس، به‌طور دقیق‌تر به دو مفهوم کلیدی این الگو می‌پردازیم: Action چگونه باید طراحی شود، و State Transition (گذار از یک وضعیت State به وضعیت دیگر) دقیقاً چه معنایی دارد.

Action: توصیف یک اتفاق، نه یک دستور مستقیم

یک اصل مهم در طراحی Action این است که Action باید توصیف‌کننده «چه اتفاقی افتاده» باشد، نه «چه کاری باید انجام شود». این تفاوت ظریف، اما مهم است:

// طراحی نامناسب: Action مثل یک دستور مستقیم به State
dispatch({ type: "setCount", value: count + 1 });

// طراحی مناسب: Action توصیف‌کننده یک رویداد واقعی
dispatch({ type: "incremented" });

در نسخه دوم، Event Handler فقط اعلام می‌کند که «افزایش رخ داده»؛ این‌که این افزایش دقیقاً چگونه روی State اعمال می‌شود (مثلاً افزودن ۱ واحد)، به‌طور کامل در Reducer تصمیم‌گیری می‌شود. این جدایی باعث می‌شود اگر روزی منطق افزایش تغییر کند (مثلاً افزایش ۲ واحدی به‌جای ۱ واحد)، فقط Reducer نیاز به تغییر داشته باشد، نه هر جایی که dispatch فراخوانی شده.

نام‌گذاری Actionها بر اساس رویداد کاربر

یک قرارداد رایج، نام‌گذاری type بر اساس زمان گذشته یک رویداد است (مانند "incremented" یا "itemAdded")، نه بر اساس نام یک تابع فنی. این نام‌گذاری کمک می‌کند Actionها بیشتر شبیه یک گزارش از اتفاقات واقعی برنامه به نظر برسند، نه فراخوانی توابع داخلی:

function handleAddClick() {
  dispatch({ type: "itemAdded", item: newItem });
}

function handleRemoveClick(id) {
  dispatch({ type: "itemRemoved", id });
}

State Transition: نگاشت از یک وضعیت به وضعیت دیگر

State Transition به این مفهوم اشاره دارد که Reducer، برای هر ترکیب از State فعلی و یک Action مشخص، دقیقاً یک State جدید تولید می‌کند. این دید، شبیه یک نمودار حالت (State Diagram) است: هر Action، یک مسیر مشخص از یک وضعیت به وضعیت دیگر است.

function formReducer(state, action) {
  switch (action.type) {
    case "submitted":
      return { ...state, status: "submitting" };
    case "succeeded":
      return { ...state, status: "success" };
    case "failed":
      return { ...state, status: "error", error: action.error };
    default:
      return state;
  }
}

در این مثال، فیلد status فقط از طریق این سه گذار مشخص تغییر می‌کند: از حالت اولیه به "submitting"، و از آن‌جا به "success" یا "error". این محدود کردن مسیرهای ممکن تغییر State، باعث می‌شود پیش‌بینی رفتار برنامه ساده‌تر شود؛ برخلاف حالتی که چند useState جداگانه (مثلاً isLoading، isSuccess، error) به‌طور مستقل تغییر کنند و امکان ترکیب‌های نامعتبر (مانند هم‌زمان true بودن isLoading و isSuccess) وجود داشته باشد.

یک قانون مهم: Reducer باید Pure باشد

یک Reducer باید یک تابع Pure باشد؛ یعنی با ورودی یکسان (همان State و همان Action)، همیشه دقیقاً همان خروجی را بدهد و هیچ عملیات جانبی (مانند درخواست به سرور یا تغییر مستقیم DOM) داخل آن انجام نشود. اگر منطقی مانند ارسال درخواست لازم است، آن باید بیرون از Reducer، معمولاً در یک Event Handler یا Effect، پیش یا پس از dispatch انجام شود.

مثال قابل اجرا

Reducer با Actionهای توصیفی و State Transitions مشخص
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

یک Action خوب، توصیف‌کننده اتفاقی است که رخ داده، نه دستوری مستقیم برای تغییر State؛ و معمولاً با نام‌هایی شبیه یک رویداد گذشته نام‌گذاری می‌شود. State Transition یعنی نگاشت مشخص از هر ترکیب State و Action به یک State جدید، که باید همیشه در یک تابع Reducer، به‌صورت Pure و بدون عملیات جانبی، تعریف شود. در درس بعدی به نحوه طراحی خود تابع Reducer می‌پردازیم.