ترکیب Reducer و Context

در فصل هشتم دیدیم Context چگونه یک مقدار را بدون Prop Drilling در اختیار بخش‌های مختلف برنامه قرار می‌دهد. در این فصل، با useReducer برای متمرکز کردن منطق تغییر State آشنا شدیم. در این درس، این دو ابزار را با هم ترکیب می‌کنیم تا الگویی بسازیم که هم State پیچیده را به‌خوبی مدیریت کند و هم بدون Prop Drilling، در دسترس هر بخشی از برنامه باشد.

چرا این دو اغلب با هم دیده می‌شوند

وقتی یک State (مثلاً وضعیت کامل یک سبد خرید یا یک فرم چندمرحله‌ای) هم پیچیده است و هم باید در چند نقطه مختلف و دور از هم برنامه در دسترس باشد، هیچ‌کدام از useReducer یا Context به‌تنهایی کل مسئله را حل نمی‌کند: useReducer منطق تغییر را متمرکز می‌کند، اما همچنان باید state و dispatch را به هر جایی که لازم است برساند؛ Context این انتقال را حل می‌کند، اما خودش هیچ منطقی برای تغییر State ندارد. ترکیب این دو، هر دو مسئله را با هم پوشش می‌دهد.

ساخت الگو: Reducer در یک Context

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function cartReducer(state, action) {
  switch (action.type) {
    case "added":
      return [...state, action.item];
    case "cleared":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, []);

  return (
    <CartContext.Provider value={cart}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartContext.Provider>
  );
}

در این ساختار، همان‌طور که در فصل هشتم درباره جدا کردن مقدار از تابع تغییردهنده دیدیم، cart و dispatch در دو Context جداگانه قرار گرفته‌اند؛ چون dispatch هیچ‌وقت تغییر نمی‌کند، اما cart با هر Action عوض می‌شود. این تفکیک، از رندر مجدد غیرضروری کامپوننت‌هایی که فقط به dispatch نیاز دارند جلوگیری می‌کند.

Custom Hookهای اختصاصی برای خواندن و تغییر

function useCart() {
  return useContext(CartContext);
}

function useCartDispatch() {
  return useContext(CartDispatchContext);
}

حالا هر کامپوننتی که فقط نیاز به نمایش سبد دارد، از useCart استفاده می‌کند، و هر کامپوننتی که باید سبد را تغییر دهد (بدون نیاز به خواندن مقدار فعلی آن)، فقط از useCartDispatch:

function CartSummary() {
  const cart = useCart();
  return <p>{cart.length} آیتم در سبد</p>;
}

function AddToCartButton({ product }) {
  const dispatch = useCartDispatch();
  return (
    <button onClick={() => dispatch({ type: "added", item: product })}>
      افزودن به سبد
    </button>
  );
}

نکته جالب این‌جاست که AddToCartButton، با وجود این‌که سبد خرید را تغییر می‌دهد، هرگز به دلیل تغییر خود cart دوباره رندر نمی‌شود؛ چون فقط به CartDispatchContext وابسته است، نه CartContext.

استفاده در ریشه برنامه

function App() {
  return (
    <CartProvider>
      <ProductPage />
      <CartSummary />
    </CartProvider>
  );
}

این الگو، مقیاس کوچکی از ابزارهای بزرگ‌تر مدیریت State است

همین الگوی «State متمرکز + توزیع بدون Prop Drilling»، پایه فکری بسیاری از ابزارهای معروف مدیریت State در اکوسیستم React است که در فصل شانزدهم به‌طور مختصر با آن‌ها آشنا خواهیم شد. تفاوت اصلی آن ابزارها با این الگوی دستی، معمولاً در بهینه‌سازی‌های بیشتر برای رندر و ابزارهای جانبی توسعه است، نه در ایده اصلی.

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

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

جمع‌بندی

ترکیب useReducer و Context به این معناست: منطق تغییر State در یک Reducer متمرکز می‌شود، و مقدار State و تابع dispatch، هرکدام از طریق یک Context جداگانه، بدون Prop Drilling، در اختیار هر بخشی از برنامه قرار می‌گیرند. جدا کردن این دو Context، از رندرهای غیرضروری هم جلوگیری می‌کند. در درس بعدی به طراحی State با Reducer در موقعیت‌های واقعی‌تر می‌پردازیم.