Advance React শিখুন বাংলায় (পর্ব ১) — useReducer ও জটিল State লজিক শিখুন

useReducer ও জটিল State লজিক

"React এর হাতেখড়ি" সিরিজ শেষ হয়েছে গত পর্বে একটা মিনি প্রজেক্ট দিয়ে। এবার শুরু হচ্ছে Advanced React সিরিজ — প্রথম টপিক useReducer, যেটা useState-এর একটা শক্তিশালী বিকল্প, বিশেষ করে যখন একটা component-এর state জটিল হয়ে যায়।

useState-এর সীমাবদ্ধতা কোথায়?

বেসিক সিরিজের পর্ব ১০-এ আমরা একটা ফর্মে একাধিক useState-এর বদলে একটা object state ব্যবহার করেছিলাম। কিন্তু state আরও জটিল হলে — একাধিক সম্পর্কিত ভ্যালু, একগুচ্ছ সম্ভাব্য "action" (যোগ করা, মোছা, টগল করা, রিসেট করা) — তখন একাধিক setState কল ছড়িয়ে-ছিটিয়ে থাকে, আর state ঠিক কীভাবে পাল্টাচ্ছে সেটা component জুড়ে খুঁজে বেড়াতে হয়।

// ❌ useState দিয়ে জটিল কার্ট লজিক — অনেকগুলো ছড়িয়ে থাকা ফাংশন
function Cart() {
  const [items, setItems] = React.useState([]);

  function addItem(product) {
    setItems([...items, { ...product, qty: 1 }]);
  }

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

  function increaseQty(id) {
    setItems(items.map((i) => i.id === id ? { ...i, qty: i.qty + 1 } : i));
  }

  function decreaseQty(id) {
    setItems(items.map((i) => i.id === id ? { ...i, qty: Math.max(1, i.qty - 1) } : i));
  }

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

  // পাঁচটা আলাদা ফাংশন, সবগুলোই items পাল্টাচ্ছে ভিন্ন ভিন্ন নিয়মে
}

useReducer — একটা কেন্দ্রীভূত সমাধান

useReducer সব state পরিবর্তনের লজিক একটা মাত্র ফাংশনে ("reducer") জড়ো করে। Component থেকে শুধু একটা "action" পাঠানো হয় (কী ঘটেছে সেটা বর্ণনা করে), আর reducer ফাংশন ঠিক করে নতুন state কী হবে।

const [state, dispatch] = useReducer(reducer, initialState);
//     ^^^^^   ^^^^^^^^            ^^^^^^^   ^^^^^^^^^^^^
//   বর্তমান  action পাঠানোর       reducer   প্রাথমিক
//   state    ফাংশন                ফাংশন     state

এখানে Advanced JS পর্ব ৪-এর Array Destructuring আবারও কাজে লাগছে — useState-এর মতোই দুইটা জিনিস রিটার্ন হয়, কিন্তু দ্বিতীয়টা এবার সরাসরি "সেট" করার ফাংশন না, বরং একটা "dispatch" ফাংশন।

Reducer ফাংশন — বেসিক গঠন

একটা reducer আসলে একটা সাধারণ ফাংশন যেটা বর্তমান state আর একটা action নিয়ে নতুন state রিটার্ন করে — এখানে Advanced JS পর্ব ৭-এর switch-স্টাইল লজিক প্রায়ই ব্যবহার হয়।

function counterReducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return { count: 0 };
    default:
      return state; // অজানা action হলে state অপরিবর্তিত রাখা
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>−</button>
      <button onClick={() => dispatch({ type: "reset" })}>রিসেট</button>
    </div>
  );
}

গুরুত্বপূর্ণ নিয়ম: reducer ফাংশন কখনো সরাসরি state পাল্টাবে না (Advanced JS পর্ব ৫-এর immutability নিয়ম) — সবসময় একটা নতুন state object রিটার্ন করবে।

useState বনাম useReducer — কখন কোনটা

পরিস্থিতি সুপারিশ
সাধারণ, স্বাধীন ভ্যালু (টগল, ইনপুট টেক্সট) useState
একাধিক সম্পর্কিত ভ্যালু, একসাথে আপডেট হয় useReducer
পরবর্তী state আগের state-এর উপর জটিলভাবে নির্ভরশীল useReducer
অনেকগুলো "action" (৪-৫টার বেশি রকমের পরিবর্তন) useReducer

লাইভ ডেমো — useReducer দিয়ে কাউন্টার

নিচে উপরের counterReducer-এর একদম বাস্তব সংস্করণ — লক্ষ্য করো, একই dispatch ফাংশন দিয়ে তিন ধরনের action পাঠানো হচ্ছে।

Action-এ Payload পাঠানো

action object-এ শুধু type না, অতিরিক্ত ডেটাও (payload) পাঠানো যায় — এটাই কার্টের মতো জটিল লজিকে দরকার হয়।

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM":
      return [...state, { ...action.payload, qty: 1 }];

    case "REMOVE_ITEM":
      return state.filter((item) => item.id !== action.payload.id);

    case "INCREASE_QTY":
      return state.map((item) =>
        item.id === action.payload.id
          ? { ...item, qty: item.qty + 1 }
          : item
      );

    case "CLEAR_CART":
      return [];

    default:
      return state;
  }
}
function Cart() {
  const [cart, dispatch] = React.useReducer(cartReducer, []);

  function handleAdd(product) {
    dispatch({ type: "ADD_ITEM", payload: product });
  }

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

  return (
    <div>
      {cart.map((item) => (
        <div key={item.id}>
          {item.name} × {item.qty}
          <button onClick={() => handleRemove(item.id)}>মুছো</button>
        </div>
      ))}
      <button onClick={() => dispatch({ type: "CLEAR_CART" })}>কার্ট খালি করো</button>
    </div>
  );
}

এখন কার্টের সব লজিক একটা মাত্র জায়গায় (cartReducer) — পাঁচটা ছড়িয়ে থাকা setState ফাংশনের বদলে, component শুধু "কী ঘটেছে" সেটা dispatch দিয়ে জানাচ্ছে।

লাইভ ডেমো — Payload-সহ কার্ট Reducer

নিচে সম্পূর্ণ কার্ট লজিক একটা reducer দিয়ে চলছে — আইটেম যোগ করো, কোয়ান্টিটি বাড়াও, মুছে ফেলো।

Reducer আলাদা ফাইলে রাখা

Advanced JS পর্ব ১১-এর Module প্যাটার্ন এখানে খুব কাজে লাগে — reducer ফাংশন component থেকে আলাদা রাখা যায়, টেস্ট করা সহজ হয়।

// cartReducer.js
export function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM":
      return [...state, action.payload];
    case "REMOVE_ITEM":
      return state.filter((item) => item.id !== action.payload.id);
    default:
      return state;
  }
}
// Cart.jsx
import { useReducer } from "react";
import { cartReducer } from "./cartReducer.js";

function Cart() {
  const [cart, dispatch] = useReducer(cartReducer, []);
  // ...
}

একটা সম্পূর্ণ উদাহরণ — ফর্ম state useReducer দিয়ে

বেসিক সিরিজের পর্ব ১০-এর multi-input ফর্মটা useReducer দিয়ে আরও সংগঠিত করা যায়, বিশেষ করে যদি রিসেট বা ভ্যালিডেশন-সম্পর্কিত action যোগ করতে হয়।

function formReducer(state, action) {
  switch (action.type) {
    case "UPDATE_FIELD":
      return { ...state, [action.field]: action.value };
    case "RESET":
      return { name: "", email: "", city: "" };
    default:
      return state;
  }
}

function SignupForm() {
  const [formData, dispatch] = React.useReducer(formReducer, {
    name: "",
    email: "",
    city: ""
  });

  function handleChange(e) {
    dispatch({
      type: "UPDATE_FIELD",
      field: e.target.name,
      value: e.target.value
    });
  }

  return (
    <form>
      <input name="name" value={formData.name} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />
      <button type="button" onClick={() => dispatch({ type: "RESET" })}>রিসেট</button>
    </form>
  );
}

নিজে চেষ্টা করো

Exercise:

  1. উপরের কাউন্টার নিজে বানাও, একটা নতুন action "incrementBy" যোগ করো যেটা payload দিয়ে যেকোনো সংখ্যা দিয়ে বাড়াবে।
  2. উপরের কার্ট reducer নিজে বানাও, একটা "DECREASE_QTY" action যোগ করো (qty ১-এর নিচে না যাওয়ার শর্তসহ)।
  3. উপরের ফর্ম reducer নিজে বানাও, একটা "VALIDATE" action যোগ করো যেটা errors একটা আলাদা key-তে state-এ রাখবে।
  4. ভেবে দেখো — তোমার React মিনি প্রজেক্টের (পর্ব ১৮) কোন অংশ useReducer দিয়ে আরও পরিষ্কার হতে পারত?

পরের পর্বে যা থাকছে

পরের পর্বে আমরা শিখব useContext ও Context API — "prop drilling" সমস্যা (props অনেক লেভেল নিচে পাঠানো) কীভাবে সমাধান করা যায়, আর থিম বা ইউজারের মতো গ্লোবাল ডেটা কীভাবে যেকোনো component থেকে সরাসরি অ্যাক্সেস করা যায়।

একটি মন্তব্য পোস্ট করুন

নবীনতর পূর্বতন

যোগাযোগ ফর্ম