طراحی State با Reducer در موقعیت‌های واقعی

در درس‌های قبلی این فصل، اجزای مختلف الگوی Reducer را جداگانه دیدیم: نحوه نوشتن خود Reducer، طراحی Actionها، و ترکیب آن با Context. در این درس، این اجزا را در یک مثال کامل‌تر و نزدیک‌تر به شرایط واقعی کنار هم می‌گذاریم تا نحوه فکر کردن درباره طراحی State با Reducer، از ابتدا تا انتها، روشن‌تر شود.

قدم اول: مشخص کردن شکل State

فرض کنید می‌خواهیم یک فرم چندمرحله‌ای ثبت‌نام بسازیم که شامل وضعیت مرحله فعلی، داده‌های وارد‌شده، و وضعیت ارسال است. پیش از نوشتن هر Actionی، اول باید شکل کلی State را مشخص کنیم:

const initialState = {
  step: 1,
  data: { name: "", email: "" },
  status: "idle", // idle | submitting | success | error
};

مشخص کردن شکل State پیش از نوشتن Reducer، کمک می‌کند تصمیم بگیریم دقیقاً چه فیلدهایی لازم است و چه ترکیب‌هایی از آن‌ها، از نظر منطقی، اصلاً نباید هم‌زمان رخ دهند.

قدم دوم: فهرست کردن اتفاقات ممکن، نه فقط فیلدها

به‌جای این‌که مستقیماً به فکر تغییر تک‌تک فیلدها باشیم، ابتدا فهرستی از اتفاقات واقعی که ممکن است در این فرم رخ دهد می‌نویسیم:

  • کاربر یک فیلد را تغییر داد
  • کاربر به مرحله بعد رفت
  • کاربر به مرحله قبل برگشت
  • ارسال فرم شروع شد
  • ارسال با موفقیت به پایان رسید
  • ارسال با خطا مواجه شد

قدم سوم: نوشتن Reducer بر اساس همان فهرست

function signupReducer(state, action) {
  switch (action.type) {
    case "fieldChanged":
      return { ...state, data: { ...state.data, [action.field]: action.value } };
    case "nextStep":
      return { ...state, step: state.step + 1 };
    case "prevStep":
      return { ...state, step: state.step - 1 };
    case "submitted":
      return { ...state, status: "submitting" };
    case "succeeded":
      return { ...state, status: "success" };
    case "failed":
      return { ...state, status: "error" };
    default:
      return state;
  }
}

توجه کنید که هر Action، مستقیماً به یکی از اتفاقات فهرست‌شده در قدم قبل مربوط است؛ نه به یک فیلد فنی خاص. این هماهنگی بین «اتفاقات واقعی» و «Actionهای تعریف‌شده»، همان چیزی است که در درس دوم این فصل، درباره طراحی درست Action تأکید کردیم.

قدم چهارم: استفاده در کامپوننت

function SignupForm() {
  const [state, dispatch] = useReducer(signupReducer, initialState);

  function handleChange(field, value) {
    dispatch({ type: "fieldChanged", field, value });
  }

  async function handleSubmit() {
    dispatch({ type: "submitted" });
    try {
      await saveSignup(state.data);
      dispatch({ type: "succeeded" });
    } catch {
      dispatch({ type: "failed" });
    }
  }

  return <div>{/* رندر بر اساس state.step و state.status */}</div>;
}

همان‌طور که در درس دوم دیدیم، عملیات ناهمزمان مانند saveSignup، بیرون از Reducer، در Event Handler اجرا می‌شود؛ Reducer فقط مسئول تعیین State جدید، بر اساس نتیجه این عملیات، است.

نتیجه این رویکرد

با پیروی از این چهار قدم (شکل State، فهرست اتفاقات، نوشتن Reducer بر اساس آن فهرست، استفاده در کامپوننت)، به یک State می‌رسیم که تمام مسیرهای معتبر تغییرش، از پیش، در یک‌جا مشخص و قابل بررسی هستند؛ برخلاف حالتی که چند useState مستقل، بدون این نظم، به‌مرور به کامپوننت اضافه شوند.

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

یک فرم دومرحله‌ای با State طراحی‌شده بر اساس Reducer
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

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