প্রত্যেক React ডেভেলপারই একসময় একই সমস্যার সম্মুখীন হন। আপনি আপনার টপ-লেভেল App কম্পোনেন্টের ভেতরে একটি user object ফেচ (fetch) করেন। তারপর সেটি নিচের কম্পোনেন্টে পাস করেন। এবং আরও নিচে। একটি route wrapper, একটি layout shell, এবং একটি sidebar container-এর মধ্য দিয়ে পাস করতে থাকেন, শুধুমাত্র যাতে তিন স্তর গভীরে থাকা একটি ছোট্ট avatar কম্পোনেন্ট প্রোফাইল পিকচারটি দেখাতে পারে। মাঝখানের কম্পোনেন্টগুলোর ওই user object নিয়ে কোনো মাথাব্যথা নেই। তারা কেবল একটি পার্সেল বা প্যাকেট পৌঁছে দিচ্ছে মাত্র। এটাই হলো prop drilling, যা একটি পরিচ্ছন্ন component tree-কে একটি বিরক্তিকর 'game of telephone'-এ পরিণত করে।

আসল সমস্যা তখন শুরু হয় যখন ওই ডেটার গঠন (shape) পরিবর্তিত হয়। হতে পারে ব্যাকএন্ড এখন user.avatar-এর পরিবর্তে user.profile.avatar ব্যবহার করছে। হঠাৎ করেই আপনাকে এমন পাঁচটি ফাইলের TypeScript interfaces বা PropTypes এডিট করতে হচ্ছে যা নিজে কখনোই ওই ডেটা ব্যবহার করে না। ঠিক এখানেই React Context API-এর প্রয়োজন পড়ে।

কীভাবে Context ডেটা প্রবাহের ধরন বদলে দেয়

Context-কে আপনার বাড়ির মাঝখানে রাখা একটি WiFi রাউটারের সাথে তুলনা করুন। এটি ছাড়া, আপনার ল্যাপটপে সিগন্যাল পেতে হলে প্রতিটি ঘরে ইথারনেট কেবল টেনে নিতে হতো। কিন্তু রাউটার থাকলে, এটি বাতাসের মাধ্যমে সিগন্যাল ব্রডকাস্ট করে এবং সঠিক পাসওয়ার্ড জানা যেকোনো ডিভাইস সরাসরি এতে যুক্ত হতে পারে। দেয়াল এখানে কোনো বাধা হয়ে দাঁড়ায় না।

React-এর ভাষায় বলতে গেলে, আপনার অ্যাপের রুট (root) প্রতিটি লেয়ারকে কুরিয়ার হিসেবে কাজ করতে না বলে সরাসরি component tree-র মাধ্যমে ডেটা ব্রডকাস্ট করতে পারে। যেকোনো নেস্টেড কম্পোনেন্ট সেই ব্রডকাস্টে সাবস্ক্রাইব করতে পারে এবং তার যা প্রয়োজন ঠিক সেটুকুই গ্রহণ করতে পারে।

তিনটি মূল অংশ

Context API মূলত তিনটি অংশ নিয়ে গঠিত।

React.createContext() ব্রডকাস্ট চ্যানেলটি তৈরি করে। এটি একটি অবজেক্ট রিটার্ন করে যাতে একটি Provider এবং (পুরানো কোডে) একটি Consumer থাকে। একটি নির্দিষ্ট ফিচারের জন্য আপনাকে এটি কেবল একবারই কল করতে হবে।

The Provider হলো একটি কম্পোনেন্ট যা আপনার ট্রি-র একটি অংশকে র‍্যাপ (wrap) করে। এটি value নামক একটি প্রপ (prop) গ্রহণ করে। আপনি ওই প্রপে যা রাখবেন, তা যত গভীরেই থাকুক না কেন, প্রতিটি বংশধর (descendant) কম্পোনেন্ট তা ব্যবহার করতে পারবে।

useContext হলো সেই Hook যা একটি ফাংশনাল কম্পোনেন্টকে ওই ব্রডকাস্ট থেকে ডেটা নিতে সাহায্য করে। আপনার কম্পোনেন্টের ভেতরে, আপনি আপনার তৈরি করা context অবজেক্টটিকে useContext-এ পাস করবেন এবং এটি বর্তমান ভ্যালু রিটার্ন করবে। ব্যস, এটুকুই। কোনো অতিরিক্ত র‍্যাপার বা প্রপসের প্রয়োজন নেই।

Hooks আসার আগে, আপনাকে render props সহ Consumer প্যাটার্ন ব্যবহার করতে হতো। এটি কাজ করত ঠিকই, কিন্তু এতে প্রচুর ইনডেন্টেশন এবং র‍্যাপারের জটলা তৈরি হতো। useContext সেই সব জটিলতাকে আপনার ফাংশন বডির ভেতরে একটি মাত্র লাইনে নিয়ে এসেছে।

কখন Context ব্যবহার করা যুক্তিসঙ্গত

অভ্যাসবশত Context ব্যবহার করবেন না। এটি এমন ডেটার জন্য তৈরি করা হয়েছে যা আপনার ট্রি-র বিভিন্ন শাখায় থাকা অনেক সম্পর্কহীন কম্পোনেন্ট শেয়ার করে। এর উপযুক্ত উদাহরণ হলো:

  • Theme settings: শুধু light বা dark mode নয়, বরং স্পেসিং টোকেন, কালার প্যালেট এবং ফন্ট স্কেল। প্রতিটি স্টাইলড বাটন বা মোডালের ভেতর দিয়ে এগুলো ম্যানুয়ালি পাস করা বেশ বিরক্তিকর।
  • User authentication: লগইন স্ট্যাটাস, পারমিশন অ্যারে, বা বর্তমান user object। আপনার হেডার বার, একটি ড্যাশবোর্ড উইজেট এবং একটি প্রাইভেট রুট গার্ড—সবই ট্রি-র ভিন্ন ভিন্ন কোণায় থাকতে পারে।
  • Language preferences: লোকাল স্ট্রিং, তারিখের ফরম্যাট এবং কারেন্সি সিম্বল। ফর্ম লেবেলের মতো গভীর স্তরের কম্পোনেন্টগুলোর এগুলো প্রয়োজন হয়, কিন্তু পথের প্রতিটি প্যারেন্ট কম্পোনেন্টের এগুলো জানার প্রয়োজন নেই।
  • Shopping cart data: আইটেম সংখ্যা, মোট মূল্য এবং add-to-cart ফাংশন। হেডার ব্যাজ এবং চেকআউট পেজ—উভয়কেই একই স্টেট প্রয়োজন, কিন্তু তারা সাধারণত সম্পূর্ণ ভিন্ন লেআউট ব্রাঞ্চের অধীনে থাকে।

একটি ব্যবহারিক Theme Switcher

Context কীভাবে কাজ করে তা দেখার অন্যতম সহজ উপায় হলো একটি theme toggle। নিচে বিস্তারিতভাবে দেখানো হলো কীভাবে আপনি এটি সেটআপ করতে পারেন।

প্রথমত, একটি ThemeContext.js ফাইল তৈরি করুন। React.createContext() কল করুন এবং ফলাফলটি সংরক্ষণ করুন। তারপর একটি ThemeProvider কম্পোনেন্ট তৈরি করুন যা useState বা useReducer দিয়ে বর্তমান থিমটি ম্যানেজ করবে। আপনার context-এর Provider দিয়ে চিলড্রেনগুলোকে র‍্যাপ করুন এবং একটি অবজেক্ট পাস করুন যাতে বর্তমান থিম এবং থিম পরিবর্তন করার একটি ফাংশন উভয়ই থাকে। ThemeProvider এবং context অবজেক্টটি উভয়ই এক্সপোর্ট করুন।

দ্বিতীয়ত, আপনার অ্যাপের এন্ট্রি পয়েন্টে যান। ThemeProvider ইম্পোর্ট করুন এবং আপনার পুরো অ্যাপ্লিকেশনটিকে এটি দিয়ে র‍্যাপ করুন। আপনি যদি এই ধাপটি বাদ দেন, তবে পরবর্তীতে যে কোনো কম্পোনেন্ট context পড়ার চেষ্টা করলে কেবল ডিফল্ট ভ্যালুটি দেখতে পাবে।

তৃতীয়ত, কোনো Header বা Content কম্পোনেন্টের ভেতরে context অবজেক্ট এবং useContext ইম্পোর্ট করুন। Hook-টি কল করুন, theme এবং toggle ফাংশনটি ডিস্ট্রাকচার (destructure) করুন এবং আপনার CSS ক্লাসগুলো কন্ডিশনালি প্রয়োগ করুন। একটি বাটন যোগ করুন যা toggle ফাংশনটিকে কল করে। কম্পোনেন্টটি তার প্যারেন্ট থেকে কখনোই theme প্রপ পায় না; এটি সরাসরি বাতাস থেকে সিগন্যালটি টেনে নেয়।

Prop Drilling, Context, নাকি Redux?

এই টুলগুলোর মধ্যে নির্বাচন করা আনুগত্যের বিষয় নয়, বরং আপনার স্টেট (state)-এর গঠনের ওপর নির্ভর করে।

Prop drilling দুই বা তিন লেভেল গভীরতার জন্য একদম ঠিক আছে। এটি স্পষ্ট, আপনার IDE-তে সহজেই ট্র্যাক করা যায় এবং ডিপেন্ডেন্সিগুলো (dependencies) পরিষ্কার রাখে। সমস্যা তখনই দেখা দেয় যখন আপনি একই প্রপ (prop) ছয় বা সাতটি লেয়ারের মধ্য দিয়ে পাঠাতে শুরু করেন।

Context API সরাসরি React-এর সাথেই আসে। এর মানে হলো বাড়তি কোনো বান্ডেল সাইজ (bundle size) বা এক্সটার্নাল সেটআপের প্রয়োজন নেই। এটি ছোট থেকে মাঝারি মানের গ্লোবাল স্টেট খুব সুন্দরভাবে হ্যান্ডেল করতে পারে, বিশেষ করে সেই ডেটাগুলো যা খুব একটা পরিবর্তন হয় না, যেমন থিম (theme) বা ইউজার প্রোফাইল।

Redux ব্যবহার করতে অতিরিক্ত লাইব্রেরি ইনস্টল করতে হয় এবং অনেক বয়লারপ্লেট (boilerplate) কোড লিখতে হয়। যখন আপনার স্টেট লজিক জটিল হয়, স্টেটের একাধিক স্লাইস (slices) গভীরভাবে একে অপরের সাথে কাজ করে, অথবা যখন আপনার টাইম-ট্রাভেল ডিবাগিং (time-travel debugging) এবং মিডলওয়্যার (middleware)-এর প্রয়োজন হয়, তখন এটি কাজে দেয়। সাধারণ গ্লোবাল ডেটার জন্য Redux ব্যবহার করা অতিরিক্ত বা অপ্রয়োজনীয় (overkill)।

পারফরম্যান্সের সেই বাস্তবতা যা নিয়ে কেউ কথা বলে না

এখানেই সেই সূক্ষ্ম পার্থক্য যা জুনিয়র এবং সিনিয়র ডেভেলপারদের কাজের মধ্যে পার্থক্য গড়ে দেয়। যখন একটি Context Provider-এর ভ্যালু পরিবর্তিত হয়, তখন সেই কনটেক্সট ব্যবহারকারী প্রতিটি কম্পোনেন্ট পুনরায় রেন্ডার (re-render) হয়। কম্পোনেন্টটি যে নির্দিষ্ট স্লাইসটি নিয়ে কাজ করছে সেটি অপরিবর্তিত থাকলেও কোনো লাভ নেই। React নতুন রেফারেন্সটি দেখে এবং একটি আপডেট শিডিউল করে।

আপনি যদি আপনার পুরো অ্যাপ্লিকেশনের স্টেট একটি বিশাল StoreContext-এ ঢেলে দেন, তবে আপনি কার্যত আপনার পুরো UI-কে একসাথে আটকে ফেললেন। থিম সেটিংস পরিবর্তন করলে আপনার শপিং কার্ট, ড্যাশবোর্ড চার্ট এবং নোটিফিকেশন লিস্ট—সবকিছুই পুনরায় রেন্ডার হবে। এটি একটি অপ্রয়োজনীয় কাজ।

আপনার কনটেক্সটগুলোকে ডোমেইন অনুযায়ী ভাগ করুন। ভিজ্যুয়াল সেটিংসের জন্য একটি ThemeContext, প্রোফাইল ডেটার জন্য একটি UserContext, এবং কমার্স স্টেটের জন্য একটি CartContext রাখুন। যদি একজন ইউজার তার ডিসপ্লে নাম পরিবর্তন করেন, তবে প্রোডাক্ট গ্রিডকে স্পর্শ না করেই আপনার হেডার আপডেট হয়ে যাবে। এছাড়া, Provider-এর value প্রপে আপনি কী পাস করছেন সে বিষয়ে সতর্ক থাকুন। আপনি যদি রেন্ডারিংয়ের সময় ইনলাইন হিসেবে একটি অবজেক্ট লিটারেল { theme, toggleTheme } পাস করেন, তবে প্রতিবার রেন্ডারের সময় একটি নতুন রেফারেন্স তৈরি হবে এবং অপ্রয়োজনীয় আপডেট ট্রিগার করবে। যদি ভ্যালুর মধ্যে ফাংশন বা নন-প্রিমিটিভ ডেটা থাকে, তবে useMemo ব্যবহার করে সেই গঠনটি স্থিতিশীল (stabilize) করুন।

যে ভুলগুলো ঘণ্টার পর ঘণ্টা সময় নষ্ট করে

দুটি ভুল বারবার টিমগুলোকে বিপদে ফেলে।

কনটেক্সট অবজেক্ট এক্সপোর্ট করতে ভুলে যাওয়া। অনেক সময় ThemeProvider কম্পোনেন্টটি এক্সপোর্ট করা হয় এবং তারপর useContext(ThemeProvider) কল করার চেষ্টা করা হয়। এটি এভাবে কাজ করে না। Hook-এর জন্য createContext থেকে প্রাপ্ত কনটেক্সট অবজেক্টটি প্রয়োজন, র‍্যাপার (wrapper) কম্পোনেন্ট নয়। আপনি যদি শুধুমাত্র Provider এক্সপোর্ট করেন, তবে আপনার কনজিউমারদের (consumers) ইমপোর্ট করার জন্য কিছুই থাকবে না।

Provider-এর বাইরে useContext কল করা। Hook-টি সেই ডিফল্ট ভ্যালু রিটার্ন করে যা আপনি createContext-এ পাস করেছিলেন। আপনি যদি কোনো ডিফল্ট ভ্যালু না দেন, তবে আপনি undefined পাবেন। যদি আপনার কম্পোনেন্ট ট্রি Provider-এর তুলনায় DOM-এ উপরে কনজিউমার রেন্ডার করে, অথবা যদি Provider একেবারেই না থাকে, তবে আপনার ডেটা পৌঁছাবে না। আপনার index বা root ফাইলটি অ্যাপটিকে সঠিকভাবে র‍্যাপ (wrap) করছে কি না তা পুনরায় যাচাই করে নিন।

মূল শিক্ষা

React Context কোনো স্টেট ম্যানেজমেন্ট বিপ্লব নয়। এটি একটি নির্দিষ্ট সমস্যার জন্য তৈরি একটি টার্গেটেড টুল: প্রতিটি লেয়ারকে পোস্ট অফিস বানিয়ে না ফেলে কীভাবে দূরবর্তী কম্পোনেন্টে ডেটা পৌঁছানো যায়। এটি শুধুমাত্র প্রকৃত গ্লোবাল ডেটার জন্য ব্যবহার করুন, রেন্ডারিং পারফরম্যান্স বজায় রাখতে আপনার কনটেক্সটগুলোকে ডোমেইন অনুযায়ী ভাগ করে রাখুন, এবং সিগন্যাল পড়ার চেষ্টা করার আগে সবসময় আপনার ট্রি-কে সঠিক Provider দিয়ে র‍্যাপ করুন। এই অভ্যাসগুলো রপ্ত করতে পারলে আপনার কম্পোনেন্ট ট্রি পরিষ্কার, দ্রুত এবং সহজে বোঝার উপযোগী থাকবে।