রিডিউসার ও অ্যাকশন — একটি মিনি স্টেট স্টোর বানানো
এই পাঠে যা শিখবেন
- 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-এর তিনটি অংশ
বর্তমান স্টেট — Store নিজেই এর একমাত্র "মালিক", বাইরে থেকে সরাসরি পরিবর্তন করা যায় না।
reducer কল করে নতুন state হিসেব করে, তারপর
self.state আপডেট করে ও সব subscriber-কে জানায়।একটি ফাংশন রেজিস্টার করে যা state পরিবর্তনের পর প্রতিবার কল হবে — View এভাবেই জানে কখন রি-রেন্ডার করতে হবে।
৩ · পূর্ণাঙ্গ কোড — একটি মিনি Todo স্টোর
নিচের কোড সেলে একটি সম্পূর্ণ, কার্যকর মিনি স্টেট স্টোর তৈরি করা হয়েছে। todo_reducer তিনটি
action type হ্যান্ডল করে — ADD_TODO, TOGGLE_TODO, REMOVE_TODO — এবং
প্রতিটি ক্ষেত্রে {**state, ...} স্প্রেড সিনট্যাক্স ব্যবহার করে একটি সম্পূর্ণ নতুন ডিকশনারি
রিটার্ন করে, কখনো বিদ্যমান state-এর কোনো কী সরাসরি assign করে না।
# --- 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}")
is_immutable এর মান True হবে — কারণ todo_reducer
প্রতিটি শাখাতেই {**state, ...} দিয়ে একটি সম্পূর্ণ নতুন ডিকশনারি অবজেক্ট তৈরি করে, কখনো
state["todos"] = ... এর মতো সরাসরি assign (mutate) করে না। TOGGLE_TODO-তে
লক্ষ্য করুন — শুধু যে টুডুটির id মিলেছে তারই একটি নতুন কপি তৈরি হয় ({**todo, "done": ...}),
বাকি টুডুগুলো অপরিবর্তিত রেফারেন্স হিসেবে থেকে যায় — কিন্তু todos লিস্ট ও বাইরের state
ডিকশনারি উভয়ই নতুন অবজেক্ট, তাই id() চেক তারপরও সঠিকভাবে True দেখায়।
একটি সত্যিকারের 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 অবস্থায়
অবশিষ্ট থাকে।
অনুশীলন
-
চিন্তা করুন:
subscribe()-এ একাধিক listener রেজিস্টার করা থাকলে (ধরুন একটি UI আপডেট করে, আরেকটি লগ রাখে) একটি singledispatch()কল কতবার প্রতিটি listener-কে ট্রিগার করবে?ঠিক একবার —
dispatch()-এর ভেতরেfor listener in self.listenersলুপ প্রতিটি রেজিস্টার্ড listener-কে state আপডেট হওয়ার পর ঠিক একবার কল করে। যদি ৩টি listenersubscribe()করা থাকে, একটিdispatch()কল সেই ৩টিকেই একবার করে কল করবে — মোট ৩টি কল, একটি dispatch থেকে। -
পরীক্ষা করুন: উপরের কোড সেলে
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-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
- পরের পাঠ — Context API ও বিকল্প স্টেট প্যাটার্ন L17 এই পাঠের Store কখন উপযুক্ত আর কখন একটি সরল shared "context" যথেষ্ট, তার তুলনা।
- Python Programming কোর্স সহোদর কোর্স ডিকশনারি স্প্রেড, লিস্ট কম্প্রিহেনশন ও ক্লাসের মতো সিনট্যাক্স সেই কোর্সেই শেখানো হয়েছে।