مفهوم Reducer و useReducer

تا اینجا، برای مدیریت State از useState استفاده کرده‌ایم؛ ابزاری که برای مقادیر ساده یا نسبتاً مستقل از هم، کاملاً کافی است. اما وقتی منطق تغییر State پیچیده‌تر می‌شود، یا چند State مرتبط باید هماهنگ با هم تغییر کنند، React ابزار دیگری هم ارائه می‌دهد: useReducer. در این درس، با مفهوم پایه Reducer و این Hook آشنا می‌شویم.

مشکل: useState وقتی منطق تغییر پیچیده می‌شود

فرض کنید یک سبد خرید داریم که باید افزودن آیتم، حذف آیتم و خالی کردن کامل سبد را پشتیبانی کند:

function Cart() {
  const [items, setItems] = useState([]);

  function addItem(item) {
    setItems([...items, item]);
  }

  function removeItem(id) {
    setItems(items.filter((item) => item.id !== id));
  }

  function clearCart() {
    setItems([]);
  }
}

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

ایده Reducer: متمرکز کردن منطق تغییر در یک تابع واحد

Reducer یک تابع معمولی است که State فعلی و یک Action (توصیفی از اتفاقی که افتاده) را دریافت می‌کند و State جدید را بازمی‌گرداند:

function cartReducer(state, action) {
  switch (action.type) {
    case "add":
      return [...state, action.item];
    case "remove":
      return state.filter((item) => item.id !== action.id);
    case "clear":
      return [];
    default:
      return state;
  }
}

در این کد، تمام منطق تغییر State سبد خرید، در یک تابع واحد و قابل پیش‌بینی جمع شده است؛ برخلاف useState که منطق در چند تابع پراکنده مستقل نوشته می‌شد.

useReducer: اتصال Reducer به کامپوننت

برای استفاده از این تابع Reducer در یک کامپوننت، از useReducer استفاده می‌کنیم:

function Cart() {
  const [items, dispatch] = useReducer(cartReducer, []);

  function handleAdd(item) {
    dispatch({ type: "add", item });
  }

  function handleRemove(id) {
    dispatch({ type: "remove", id });
  }

  return (
    <div>
      {items.map((item) => (
        <p key={item.id}>{item.name}</p>
      ))}
      <button onClick={() => dispatch({ type: "clear" })}>خالی کردن</button>
    </div>
  );
}

useReducer دو آرگومان می‌گیرد: خود تابع Reducer، و مقدار اولیه State (در اینجا آرایه خالی). در خروجی، مانند useState، یک آرایه دو‌عضوی بازمی‌گرداند: مقدار فعلی State (items)، و این‌بار به‌جای یک تابع set، یک تابع به نام dispatch که برای اعلام یک Action استفاده می‌شود.

Action چیست

یک Action معمولاً یک Object ساده است که حداقل یک فیلد type دارد تا مشخص کند چه اتفاقی رخ داده، و در صورت نیاز، اطلاعات اضافی دیگری هم می‌تواند داشته باشد (مانند item یا id در مثال بالا). فراخوانی dispatch(action) به این معناست: «این اتفاق افتاده، حالا خودت (Reducer) تصمیم بگیر State جدید چه باید باشد».

مزیت اصلی: جدایی «چه اتفاقی افتاده» از «چگونه State تغییر می‌کند»

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

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

مدیریت سبد خرید با useReducer
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Reducer تابعی است که State فعلی و یک Action را می‌گیرد و State جدید را برمی‌گرداند؛ useReducer این تابع را به کامپوننت متصل می‌کند و به‌جای یک تابع set، یک تابع dispatch در اختیار می‌گذارد. این الگو، منطق تغییر State را از پراکندگی در چند تابع، به یک محل واحد و متمرکز منتقل می‌کند. در درس بعدی، با جزئیات بیشتری به Actions و State Transitions می‌پردازیم.