পাঠ ১৭ · ৫৮-এর মধ্যে · মডিউল ৪

Context API ও বিকল্প স্টেট প্যাটার্ন

Context API and alternative state patterns
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • Context কীভাবে prop-drilling ছাড়াই শেয়ার্ড ডেটা সরবরাহ করে — একটি শেয়ার্ড রেফারেন্স হিসেবে
  • Context ও Redux-স্টাইল Store-এর মধ্যে গঠনগত পার্থক্য
  • কখন Context যথেষ্ট, আর কখন একটি পূর্ণাঙ্গ স্টোর দরকার — একটি স্পষ্ট সিদ্ধান্ত-নিয়ম
  • Python ক্লোজার/শেয়ার্ড-রেফারেন্স দিয়ে Context-এর একটি সত্যিকারের, কার্যকর সিমুলেশন

১ · Context কী — একটি শেয়ার্ড রেফারেন্স

ContextContextএকটি শেয়ার্ড ডেটা-উৎস যা নেস্টেড কম্পোনেন্টগুলো সরাসরি পড়তে পারে, প্রতিটি মধ্যবর্তী লেভেলে এক্সপ্লিসিট প্যারামিটার হিসেবে পাস না করেই। হলো L14-এ দেখা প্রপ-ড্রিলিং সমস্যার একটি সরল সমাধান — এমন কিছু ডেটার জন্য যেগুলো Redux-স্টাইল একটি পূর্ণাঙ্গ স্টোরের জটিলতা দাবি করে না। মূল ধারণা: ডেটা একটি "শেয়ার্ড" জায়গায় রাখা হয়, এবং যেকোনো গভীরতার নেস্টেড ফাংশন/কম্পোনেন্ট সরাসরি সেই জায়গা থেকে পড়ে — প্রতিটি মধ্যবর্তী লেভেলের ফাংশন সিগনেচারে সেই ডেটা প্যারামিটার হিসেবে যোগ করার দরকার হয় না।

Context উপযুক্ত যখন
ডেটা কম ঘন ঘন বদলায় (থিম, ভাষা, বর্তমান ইউজার) এবং আপডেট-লজিক সরল — কোনো একাধিক action-type বা জটিল reducer দরকার নেই।
Store (L16) উপযুক্ত যখন
ডেটা ঘন ঘন, জটিলভাবে বদলায় (todo লিস্ট, শপিং কার্ট) এবং প্রতিটি পরিবর্তনে subscriber-দের নির্দিষ্ট নোটিফিকেশন দরকার।

২ · Context বনাম Store — সত্যিকারের কোড দিয়ে তুলনা

নিচের কোড সেলে প্রথমে Context সিমুলেট করা হয়েছে একটি সাধারণ শেয়ার্ড ডিকশনারি (theme_context) দিয়ে — নেস্টেড ফাংশনগুলো এটি সরাসরি পড়ে, প্যারামিটার হিসেবে গ্রহণ না করেই (ঠিক যেমন Python-এর ফাংশনগুলো কোনো এনক্লোজিং/গ্লোবাল স্কোপের ভ্যারিয়েবল সরাসরি পড়তে পারে)। তারপর L16-এর Store প্যাটার্নের একটি সংক্ষিপ্ত সংস্করণ দিয়ে দেখানো হয়েছে ঘন ঘন পরিবর্তিত হওয়া ডেটায় subscribe() নোটিফিকেশন কীভাবে কাজ করে — এবং Context-এ এই ধরনের নোটিফিকেশন না থাকাটা কীভাবে গণনা করে ধরা যায়।

Python
# --- অংশ ১: Context সিমুলেশন -- prop drilling ছাড়াই শেয়ার্ড ডেটা পড়া ---
theme_context = {"mode": "dark", "accent": "green"}

def header_component():
    # theme_context সরাসরি পড়ছে -- কোনো প্যারামিটার হিসেবে পায়নি
    return f"[Header] মোড={theme_context['mode']}, অ্যাকসেন্ট={theme_context['accent']}"

def sidebar_component():
    return f"[Sidebar] মোড={theme_context['mode']}"

def footer_component():
    return f"[Footer] মোড={theme_context['mode']}"

def app_component():
    # app_component নিজেও theme_context প্যারামিটার হিসেবে নেয়নি বা নিচে পাস করেনি
    return "\n".join([header_component(), sidebar_component(), footer_component()])

print("থিম পরিবর্তনের আগে:")
print(app_component())

# থিম বদলালে -- সব consumer কম্পোনেন্ট আবার call হলে স্বয়ংক্রিয়ভাবে নতুন মান দেখে
theme_context["mode"] = "light"
print("\nথিম পরিবর্তনের পর (কোনো ফাংশন সিগনেচার পরিবর্তন করতে হয়নি):")
print(app_component())
print("=" * 60)


# --- অংশ ২: ঘন ঘন পরিবর্তনশীল, জটিল স্টেটের জন্য -- L16-এর Store প্যাটার্ন (সংক্ষিপ্ত) ---
def counter_reducer(state, action):
    if action["type"] == "INCREMENT":
        return {**state, "value": state["value"] + 1}
    return state

class MiniStore:
    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):
        self.state = self.reducer(self.state, action)
        for listener in self.listeners:
            listener(self.state)

notify_count = 0
def on_change(state):
    global notify_count
    notify_count += 1
    print(f"  [store subscriber] নোটিফিকেশন #{notify_count} -- value={state['value']}")

counter_store = MiniStore(counter_reducer, {"value": 0})
counter_store.subscribe(on_change)

for _ in range(5):
    counter_store.dispatch({"type": "INCREMENT"})

context_change_count = 1  # theme_context উপরে ঠিক একবার পরিবর্তন করা হয়েছিল
print(f"\nমোট Store dispatch: 5, মোট subscriber নোটিফিকেশন: {notify_count}")
print(f"তুলনায়, theme_context {context_change_count}বার পরিবর্তন হলেও কোনো subscriber নোটিফিকেশন হয়নি -- "
      f"consumer ফাংশনগুলো শুধু পরের বার call হলে সরাসরি সর্বশেষ মান পড়ে।")

    
লক্ষ্য করুন app_component(), header_component(), sidebar_component(), footer_component() — এদের কারোর সিগনেচারেই theme প্যারামিটার নেই, তবুও প্রতিটি সরাসরি সর্বশেষ theme_context মান পায়। এটিই Context-এর মূল সুবিধা — সরলতা। কিন্তু এর মূল্য হলো: কোনো subscribe()/নোটিফিকেশন কাঠামো নেই, তাই ৫টি dispatch() কল সঠিকভাবে ৫টি subscriber নোটিফিকেশন তৈরি করে, অথচ theme_context-এ একই ধরনের পরিবর্তন হলে কোনো নোটিফিকেশনই হয় না — শুধু পরের বার কল হলে নতুন মান দেখা যায়।
মূল কথা · Key takeaway

Context ও Redux-স্টাইল Store — দুটোই প্রপ-ড্রিলিং এড়ায়, কিন্তু ভিন্ন সমস্যার জন্য ডিজাইন করা। কম ঘন ঘন বদলানো, সরল ডেটার (থিম, লোকেল, বর্তমান ইউজার) জন্য Context-এর সরলতা যথেষ্ট এবং পছন্দনীয়। ঘন ঘন, জটিলভাবে বদলানো ডেটার (todo লিস্ট, শপিং কার্ট, ফর্ম-উইজার্ড স্টেট) জন্য L16-এর মতো একটি পূর্ণাঙ্গ reducer + dispatch + subscribe কাঠামো প্রয়োজন — কারণ তখন নির্দিষ্ট, ট্রেসযোগ্য নোটিফিকেশন গুরুত্বপূর্ণ হয়ে ওঠে। বাস্তব প্রজেক্টে প্রায়ই দুটোই একসাথে ব্যবহৃত হয় — যেমন থিমের জন্য Context, আর অ্যাপ্লিকেশন ডেটার জন্য একটি স্টোর।

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

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

প্র ০১ Context ব্যবহার করে একটি ঘন ঘন পরিবর্তিত হওয়া (যেমন প্রতি সেকেন্ডে বদলানো) কাউন্টার রাখলে কী সমস্যা হতে পারে?

বাস্তব ফ্রেমওয়ার্কে (React) Context আপডেট হলে সেই Context ব্যবহারকারী সব কম্পোনেন্ট রি-রেন্ডার হয় — তাদের প্রতিটি আসলে সেই নির্দিষ্ট পরিবর্তনে আগ্রহী কি না তা নির্বিশেষে। ঘন ঘন পরিবর্তনে এটি অপ্রয়োজনীয় অনেক রি-রেন্ডার তৈরি করে পারফরম্যান্স খারাপ করতে পারে। Redux-স্টাইল স্টোরে সাধারণত আরও সূক্ষ্ম (fine-grained) সাবস্ক্রিপশন সম্ভব — শুধু প্রাসঙ্গিক অংশ পরিবর্তিত হলেই সংশ্লিষ্ট অংশ রি-রেন্ডার হয়।

প্র ০২ উপরের কোডে header_component()-কে যদি app_component() ছাড়া, সম্পূর্ণ আলাদা একটি ফাংশন থেকেও কল করা হয়, তাহলেও কি এটি সঠিক থিম পাবে?

হ্যাঁ — কারণ header_component() থিম পড়ে theme_context নামের গ্লোবাল ভ্যারিয়েবল থেকে সরাসরি, কোনো প্যারামিটার বা কলিং-চেইনের ওপর নির্ভর করে নয়। এটি যে কোনো জায়গা থেকে কল হোক না কেন, সবসময় theme_context-এর বর্তমান মান দেখবে — এটিই Context-ভিত্তিক পদ্ধতির মূল বৈশিষ্ট্য, প্রপ-ড্রিলিং পদ্ধতির বিপরীতে যেখানে ডেটা শুধু কলিং-চেইনের মধ্য দিয়েই পৌঁছাতে পারত।

প্র ০৩ কোড সেলে notify_count-এর চূড়ান্ত মান কত হবে, এবং কেন?

৫ হবে। for _ in range(5) লুপ ৫ বার counter_store.dispatch({"type": "INCREMENT"}) কল করে, আর প্রতিটি dispatch() কল ভেতরে on_change subscriber-কে ঠিক একবার কল করে (যেহেতু শুধু একটিই subscriber রেজিস্টার করা আছে), যা প্রতিবার notify_count-কে ১ করে বাড়ায়। তাই ৫টি dispatch = ৫টি নোটিফিকেশন।

অনুশীলন

  1. চিন্তা করুন: একটি অ্যাপে "বর্তমান লগ-ইন করা ইউজার"-এর নাম প্রায় প্রতিটি পেজে দেখাতে হয়, কিন্তু এটি সেশন চলাকালীন খুব কমই পরিবর্তিত হয় (শুধু লগ-ইন/লগ-আউটে)। এটির জন্য Context নাকি Redux-স্টাইল Store বেশি উপযুক্ত?

    Context বেশি উপযুক্ত — কারণ ডেটা (ইউজারের তথ্য) কম ঘন ঘন বদলায় এবং আপডেট-লজিক সরল (লগ-ইন করলে সেট, লগ-আউট করলে ক্লিয়ার — একাধিক জটিল action-type দরকার নেই)। একটি পূর্ণাঙ্গ Store ব্যবহার করা ভুল হবে না, কিন্তু এত সরল ব্যবহারের জন্য অতিরিক্ত জটিলতা যোগ করবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে theme_context-এ একটি নতুন কী "accent"-কে "purple"-এ পরিবর্তন করুন (থিম পরিবর্তনের ঠিক পরে), এবং header_component()-কে আরও একবার কল করে ফলাফল প্রিন্ট করুন — নতুন accent মান কি দেখা যায়?

    হ্যাঁ, header_component()-এর নতুন কলে অ্যাকসেন্ট=purple দেখাবে — কারণ ফাংশনটি প্রতিবার কল হওয়ার সময় theme_context-এর বর্তমান (সর্বশেষ) মান পড়ে, কোনো পুরনো "স্ন্যাপশট" ধরে রাখে না। এটিই দেখায় Context-ভিত্তিক শেয়ার্ড রেফারেন্স সবসময় সবচেয়ে সাম্প্রতিক ডেটা প্রতিফলিত করে, প্যারামিটার হিসেবে একবার পাস হওয়া কোনো "কপি" নয়।

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

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