পাঠ ১৬ · ৫৮-এর মধ্যে · মডিউল ৪
Home / Courses / Full-Stack Web Frameworks / রিডিউসার ও অ্যাকশন

রিডিউসার ও অ্যাকশন — একটি মিনি স্টেট স্টোর বানানো

Reducers and actions: building a mini state store
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • reducer ফাংশনের সংজ্ঞা ও কেন সেটি "pure" (কোনো side-effect নেই, একই ইনপুটে একই আউটপুট) হতে হয়
  • একাধিক action type সমর্থনকারী একটি বাস্তব reducer লেখা
  • একটি পূর্ণাঙ্গ Store ক্লাস — state, dispatch(), subscribe()
  • immutability সত্যিই বজায় আছে কি না তা id() দিয়ে যাচাই করার কৌশল

১ · Reducer — একটি Pure Function

ReducerReducerএকটি pure function যা বর্তমান state ও একটি action নিয়ে একটি নতুন state রিটার্ন করে, পুরনো state mutate না করে। হলো Flux/Redux প্যাটার্নের হৃদয় — এটি একটি ফাংশন যার স্বাক্ষর সবসময় (state, action) -> new_state আকারে থাকে। এটি একটি pure function হতে হয় — মানে (১) একই state ও action দিলে সবসময় একই new_state ফেরত দেবে, এবং (২) এটি বাইরের কোনো ভ্যারিয়েবল পরিবর্তন করবে না, শুধু একটি নতুন অবজেক্ট রিটার্ন করবে। এই বিশুদ্ধতাই state-এর ইতিহাস অনুমানযোগ্য ও টেস্টযোগ্য রাখে।

২ · Store-এর তিনটি অংশ

state
বর্তমান স্টেট — Store নিজেই এর একমাত্র "মালিক", বাইরে থেকে সরাসরি পরিবর্তন করা যায় না।
dispatch(action)
reducer কল করে নতুন state হিসেব করে, তারপর self.state আপডেট করে ও সব subscriber-কে জানায়।
subscribe(listener)
একটি ফাংশন রেজিস্টার করে যা state পরিবর্তনের পর প্রতিবার কল হবে — View এভাবেই জানে কখন রি-রেন্ডার করতে হবে।

৩ · পূর্ণাঙ্গ কোড — একটি মিনি Todo স্টোর

নিচের কোড সেলে একটি সম্পূর্ণ, কার্যকর মিনি স্টেট স্টোর তৈরি করা হয়েছে। todo_reducer তিনটি action type হ্যান্ডল করে — ADD_TODO, TOGGLE_TODO, REMOVE_TODO — এবং প্রতিটি ক্ষেত্রে {**state, ...} স্প্রেড সিনট্যাক্স ব্যবহার করে একটি সম্পূর্ণ নতুন ডিকশনারি রিটার্ন করে, কখনো বিদ্যমান state-এর কোনো কী সরাসরি assign করে না।

Python
# --- reducer: একাধিক action type, প্রতিবার নতুন state dict রিটার্ন করে ---
def todo_reducer(state, action):
    if action["type"] == "ADD_TODO":
        new_todo = {"id": action["id"], "text": action["text"], "done": False}
        return {**state, "todos": state["todos"] + [new_todo]}

    elif action["type"] == "TOGGLE_TODO":
        new_todos = [
            {**todo, "done": not todo["done"]} if todo["id"] == action["id"] else todo
            for todo in state["todos"]
        ]
        return {**state, "todos": new_todos}

    elif action["type"] == "REMOVE_TODO":
        new_todos = [todo for todo in state["todos"] if todo["id"] != action["id"]]
        return {**state, "todos": new_todos}

    else:
        return state  # অচেনা action -- state অপরিবর্তিত থাকে (একই অবজেক্ট রিটার্ন)


# --- Store ক্লাস: state + dispatch() + subscribe() ---
class Store:
    def __init__(self, reducer, initial_state):
        self.reducer = reducer
        self.state = initial_state
        self.listeners = []

    def subscribe(self, listener):
        self.listeners.append(listener)

    def dispatch(self, action):
        old_state = self.state
        self.state = self.reducer(self.state, action)
        for listener in self.listeners:
            listener(self.state)
        return old_state, self.state


def on_state_change(new_state):
    print(f"  [subscriber] নোটিফিকেশন পেল -- এখন {len(new_state['todos'])}টি টুডু")


store = Store(todo_reducer, {"todos": []})
store.subscribe(on_state_change)

actions = [
    {"type": "ADD_TODO", "id": 1, "text": "বাজার করা"},
    {"type": "ADD_TODO", "id": 2, "text": "রিপোর্ট জমা দেওয়া"},
    {"type": "TOGGLE_TODO", "id": 1},
    {"type": "REMOVE_TODO", "id": 2},
]

for action in actions:
    old_state, new_state = store.dispatch(action)
    is_immutable = id(old_state) != id(new_state)
    print(f"অ্যাকশন: {action}")
    print(f"  নতুন state: {new_state}")
    print(f"  ইমিউটেবিলিটি চেক -- id(old_state) != id(new_state): {is_immutable}")
    print("-" * 60)

print(f"চূড়ান্ত state: {store.state}")

    
প্রতিটি dispatch-এর পর is_immutable এর মান True হবে — কারণ todo_reducer প্রতিটি শাখাতেই {**state, ...} দিয়ে একটি সম্পূর্ণ নতুন ডিকশনারি অবজেক্ট তৈরি করে, কখনো state["todos"] = ... এর মতো সরাসরি assign (mutate) করে না। TOGGLE_TODO-তে লক্ষ্য করুন — শুধু যে টুডুটির id মিলেছে তারই একটি নতুন কপি তৈরি হয় ({**todo, "done": ...}), বাকি টুডুগুলো অপরিবর্তিত রেফারেন্স হিসেবে থেকে যায় — কিন্তু todos লিস্ট ও বাইরের state ডিকশনারি উভয়ই নতুন অবজেক্ট, তাই id() চেক তারপরও সঠিকভাবে True দেখায়।
মূল কথা · Key takeaway

একটি সত্যিকারের Redux-স্টাইল স্টোর তিনটি স্তম্ভের ওপর দাঁড়িয়ে থাকে — একটি pure reducer, একটি dispatch() যা state আপডেট করে, আর subscribe() যা View-কে পরিবর্তনের কথা জানায়। immutability (পুরনো state কখনো mutate না করা) কোনো ঐচ্ছিক ভালো-অভ্যাস নয় — এটিই সেই ভিত্তি যা এই পুরো প্যাটার্নকে অনুমানযোগ্য ও ডিবাগযোগ্য করে তোলে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ যদি todo_reducer-এ ADD_TODO-এর বদলে সরাসরি state["todos"].append(new_todo) লেখা হতো, তাহলে কী সমস্যা হতো?

.append() বিদ্যমান লিস্টটিকে সরাসরি পরিবর্তন (mutate) করে, নতুন লিস্ট তৈরি করে না। তার মানে old_state["todos"] ও new_state["todos"] একই অবজেক্টকে নির্দেশ করত (যদি state dict নিজেই কপি না হয়) — এতে id() চেক ভুল ফলাফল দিতে পারত, এবং আরও গুরুত্বপূর্ণভাবে, কোনো কোড যদি আগের state-এর একটি রেফারেন্স ধরে রেখেছিল (যেমন undo ফিচারের জন্য), সেই পুরনো state-ও অজান্তেই পরিবর্তিত হয়ে যেত।

প্র ০২ todo_reducer-এর else: return state লাইনটি কেন গুরুত্বপূর্ণ?

একটি reducer-কে সব সম্ভাব্য action-এর জন্য কিছু না কিছু রিটার্ন করতেই হয় — এমনকি অচেনা বা অপ্রাসঙ্গিক action-এর জন্যও। যদি এই else শাখা না থাকত, অচেনা কোনো action টাইপ পাঠালে ফাংশনটি None রিটার্ন করত, এবং Store-এর self.state ভুলবশত None হয়ে যেত — পুরো অ্যাপ্লিকেশন ভেঙে পড়ত। অচেনা action-এ state অপরিবর্তিত (একই অবজেক্ট) রিটার্ন করাই নিরাপদ ডিফল্ট আচরণ।

প্র ০৩ উপরের কোডে চারটি dispatch-এর পর store.state["todos"]-এ ঠিক কয়টি টুডু থাকবে, এবং কী অবস্থায়?

একটি থাকবে — id ১ ("বাজার করা"), এবং সেটির done হবে True। কারণ: দুটি ADD_TODO দুটি টুডু তৈরি করে (id ১, id ২); TOGGLE_TODO (id ১) সেই টুডুর done-কে False থেকে True করে; এবং REMOVE_TODO (id ২) দ্বিতীয় টুডুটি সরিয়ে দেয়। ফলে চূড়ান্ত todos-এ কেবল id ১-এর টুডুটি, done=True অবস্থায় অবশিষ্ট থাকে।

অনুশীলন

  1. চিন্তা করুন: subscribe()-এ একাধিক listener রেজিস্টার করা থাকলে (ধরুন একটি UI আপডেট করে, আরেকটি লগ রাখে) একটি single dispatch() কল কতবার প্রতিটি listener-কে ট্রিগার করবে?

    ঠিক একবার — dispatch()-এর ভেতরে for listener in self.listeners লুপ প্রতিটি রেজিস্টার্ড listener-কে state আপডেট হওয়ার পর ঠিক একবার কল করে। যদি ৩টি listener subscribe() করা থাকে, একটি dispatch() কল সেই ৩টিকেই একবার করে কল করবে — মোট ৩টি কল, একটি dispatch থেকে।

  2. পরীক্ষা করুন: উপরের কোড সেলে todo_reducer-এ একটি নতুন action type "CLEAR_COMPLETED" যোগ করুন যা done: True থাকা সব টুডু বাদ দিয়ে বাকিগুলো রাখে ({**state, "todos": [t for t in state["todos"] if not t["done"]]}), তারপর actions লিস্টে শেষে {"type": "CLEAR_COMPLETED"} যোগ করে Run চাপুন।

    দুটি ADD_TODO, একটি TOGGLE_TODO (id ১-কে done করে), একটি REMOVE_TODO (id ২ সরায়), এবং সবশেষে CLEAR_COMPLETED চালানোর পর — যেহেতু এতক্ষণে বাকি একমাত্র টুডু (id ১) ইতিমধ্যে done: True, সেটিও বাদ পড়ে যাবে এবং চূড়ান্ত store.state["todos"] একটি খালি লিস্ট [] হবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
Flux/Redux প্যাটার্ন