অপ্রাসঙ্গিক কম্পোনেন্টের বিভিন্ন স্তরের মধ্য দিয়ে প্রপস (props) পাস করা একটি ক্লান্তিকর কাজ। একদিন আপনি একটি ফিচার শিপ করছেন, আর পরের দিন একটি মাত্র প্রপসের নাম পরিবর্তন করতে গিয়ে আপনাকে ছয়টি ফাইল এডিট করতে হচ্ছে। এটাই হলো প্রপ ড্রিলিং (prop drilling)-এর মূল কথা: একটি প্যারেন্ট কম্পোনেন্টের কাছে ডেটা আছে, একটি গভীর স্তরের চাইল্ড কম্পোনেন্টের সেটি প্রয়োজন, আর তাদের মাঝখানের প্রতিটি কম্পোনেন্ট কেবল একজন কুরিয়ার বা বাহক হিসেবে কাজ করে। অ্যাপটি তখনও চলে, কিন্তু কোডবেসটি ভঙ্গুর হয়ে পড়ে। মাঝখানের একটি লেয়ার সরিয়ে ফেললে পুরো ট্রি (tree) ভেঙে পড়তে পারে। একটি টাইপ পরিবর্তন করলে তিনটি ডিরেক্টরি জুড়ে TypeScript এর এরর আসতে থাকে। React Context API তৈরি করা হয়েছে এই মধ্যস্থতাকারীদের পুরোপুরি সরিয়ে দেওয়ার জন্য।
প্রপ ড্রিলিং আসলে দেখতে কেমন
একটি সাধারণ অ্যাপ শেলের (app shell) কথা কল্পনা করুন। আপনার কাছে একটি App কম্পোনেন্ট আছে যা বর্তমান ইউজারকে ফেচ (fetch) করে। App-এর ভেতরে থাকে একটি Layout, Layout-এর ভেতরে থাকে একটি Sidebar, Sidebar-এর ভেতরে থাকে একটি Navigation, এবং সবশেষে Navigation-এর ভেতরে আপনি UserAvatar খুঁজে পান যার আসলে ইউজার অবজেক্টটি প্রয়োজন।
আপনার কোডটি দেখতে অনেকটা এরকম হয়:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar, এবং Navigation সেই ইউজার অবজেক্টটি দিয়ে কিছুই করে না, কেবল সেটি নিচের স্তরে পাঠিয়ে দেয়। তারা এমন সব প্রপস সংগ্রহ করে যা তাদের নিজস্ব নয়, ফলে তাদের ইন্টারফেস বড় হয়ে যায় এবং তাদের টেস্ট করার জন্য এমন ডেটা মক (mock) করতে হয় যা তারা কখনোই ব্যবহার করে না। আসল সমস্যা হলো এটি কত দ্রুত ছড়িয়ে পড়ে। একটি isLoggedIn ফ্ল্যাগ, একটি locale স্ট্রিং, বা একটি theme ভ্যালু যোগ করুন, আর একই দৃশ্য বারবার দেখতে পাবেন।
Context API যেভাবে গেম চেঞ্জ করে
Context API-কে একটি WiFi রাউটারের মতো ভাবুন। প্রতিটি ডিভাইসে পৌঁছানোর জন্য প্রতিটি রুমের মধ্য দিয়ে লম্বা লম্বা ক্যাবল টেনে নেওয়ার পরিবর্তে, রাউটারটি বাতাসের মাধ্যমে সিগন্যাল পাঠায়। রেঞ্জের মধ্যে থাকা যেকোনো ডিভাইস সরাসরি কানেক্ট করতে পারে। React-এর ভাষায়, রাউটার হলো Provider, সিগন্যাল হলো আপনার স্টেট বা ডেটা, এবং ডিভাইস হলো যেকোনো নেস্টেড কম্পোনেন্ট যা useContext কল করে।
এর সেটআপে তিনটি অংশ রয়েছে:
React.createContext()ডেটা চ্যানেল তৈরি করে।- Provider আপনার ট্রি-র একটি অংশকে র্যাপ (wrap) করে এবং একটি ভ্যালু ট্রান্সমিট করে।
useContextহুকটি বংশধর কম্পোনেন্টগুলোকে মাঝখানের প্রপস স্পর্শ না করেই সেই ভ্যালু গ্রহণ করতে দেয়।
আপনার কাছে এখনও একটি ট্রি আছে, কিন্তু রুট (root) এবং লিফ (leaf) এর মাঝখানের শাখাগুলোর আর কোনো কুরিয়ার চুক্তির প্রয়োজন নেই।
শুরু থেকে একটি Context তৈরি করা
চলুন থিম সেটিংস দিয়ে একটি বাস্তব উদাহরণ তৈরি করি, যেহেতু বেশিরভাগ অ্যাপের কোনো না কোনো সময় লাইট বা ডার্ক মোড প্রয়োজন হয়।
প্রথমে, কনটেক্সট অবজেক্টটি তৈরি করুন। এটি হলো পাইপলাইন:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
এরপর আপনার অ্যাপ্লিকেশনটিকে প্রোভাইডার দিয়ে র্যাপ করুন। সাধারণত এটি রুট লেভেলের কাছাকাছি করা হয়:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
এখন যেকোনো বংশধর কম্পোনেন্ট সরাসরি সিগন্যালটি গ্রহণ করতে পারে। এখানে UI-এর অনেক গভীরে থাকা একটি টগল বাটন দেওয়া হলো:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
লক্ষ্য করুন যে Layout, Sidebar, এবং Navigation কখনোই theme প্রপসটি দেখেনি। তারা স্বাভাবিকভাবে রেন্ডার হয়, এবং ThemeToggle সরাসরি কনটেক্সট থেকে তার প্রয়োজনীয় জিনিসটি নিয়ে নেয়। এর ওয়্যারিং বাইরে থেকে অদৃশ্য, আর এটাই হলো আসল উদ্দেশ্য।
Context আসলে কোথায় ব্যবহার করা উচিত
Context তখন সবচেয়ে ভালো কাজ করে যখন এমন ডেটা শেয়ার করতে হয় যা অনেক দূরবর্তী কম্পোনেন্ট ব্যবহার করে কিন্তু কোনো একক প্যারেন্ট কম্পোনেন্টের কাছে তা গুছিয়ে রাখা সম্ভব নয়। ভালো কিছু উদাহরণ হলো:
- Theme settings যেমন লাইট বা ডার্ক মোড, অ্যাকসেন্ট কালার, বা ফন্ট স্কেলিং।
- Authentication state যেমন বর্তমান ইউজার অবজেক্ট, লগইন স্ট্যাটাস, বা সেশন এক্সপায়ারি।
- Language and locale সেটিংস ইন্টারন্যাশনালইজেশনের জন্য।
- Shopping cart data যা হেডার ব্যাজ, একটি মিনি-কার্ট ড্রপডাউন এবং একটি চেকআউট পেজ জুড়ে সিঙ্ক (sync) থাকতে হবে।
প্রতিটি লোকাল স্টেট (local state) Context-এ ঢেলে দেওয়ার প্রবণতা থেকে বিরত থাকুন। দুই স্তর নিচে থাকা একটি ফর্ম ইনপুটের জন্য গ্লোবাল ব্রডকাস্টের প্রয়োজন নেই। প্রকৃত ক্রস-কাটিং কনসার্নস (cross-cutting concerns)-এর জন্য Context রাখুন, আর বাকি সব সাধারণ প্রপস হিসেবেই থাকতে দিন।
পারফরম্যান্সের ফাঁদ এবং সেগুলো এড়ানোর উপায়
Context ব্যবহার করা একদম বিনামূল্যে নয়। যখন একটি কনটেক্সট ভ্যালু আপডেট হয়, তখন সেই কনটেক্সটে যুক্ত প্রতিটি কম্পোনেন্ট পুনরায় রেন্ডার (re-render) হয়, এমনকি যদি ভ্যালুর সেই অংশটি পরিবর্তন না-ও হয় যা তারা ব্যবহার করছে। ক্লাসিক ভুল হলো প্রতিটি প্যারেন্ট রেন্ডারের সময় Provider-এ একটি নতুন অবজেক্ট লিটারেল (object literal) পাঠিয়ে দেওয়া।
আমাদের থিম উদাহরণের ক্ষেত্রে, যখনই ThemeProvider তার নিজস্ব প্যারেন্ট আপডেট হওয়ার কারণে পুনরায় রেন্ডার হয়, তখনই { theme, setTheme } এক্সপ্রেশনটি একটি সম্পূর্ণ নতুন অবজেক্ট তৈরি করে। React একটি নতুন রেফারেন্স দেখতে পায় এবং প্রতিটি কনজিউমার আপডেট হয়ে যায়। যদি আপনার থিম খুব কম পরিবর্তিত হয় কিন্তু অ্যাপের স্টেট ঘন ঘন পরিবর্তিত হয়, তবে আপনি অপ্রয়োজনীয় রেন্ডারের জন্য পেমেন্ট করছেন।
এর সমাধান দুটিভাবে করা যায়।
আপডেট হওয়ার ফ্রিকোয়েন্সি অনুযায়ী আপনার কনটেক্সটগুলোকে আলাদা করুন। একটি UserContext যা প্রতিবার লগইনের সময় পরিবর্তিত হয়, তাকে একটি NotificationContext-এর সাথে একই প্রোভাইডারে রাখা উচিত নয় যা প্রতি কয়েক সেকেন্ড পরপর আপডেট হয়। এদের আলাদা রাখুন যাতে স্ট্যাটিক ডেটা অস্থির (volatile) ডেটার সাথে একই রেন্ডার ট্রেনের যাত্রী না হয়।
ভ্যালু যদি অবজেক্ট বা অ্যারে হয়, তবে useMemo দিয়ে সেটিকে র্যাপ করুন। React-কে একটি স্টেবল রেফারেন্স দিন:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
এখন অবজেক্ট আইডেন্টিটি কেবল তখনই পরিবর্তিত হয় যখন theme প্রকৃতপক্ষে পরিবর্তিত হয়। যে সকল ডেসেন্ডেন্ট কম্পোনেন্ট কনটেক্সট সম্পর্কে সচেতন কিন্তু নিচে React.memo দ্বারা সুরক্ষিত, তারা এই কাজটি এড়িয়ে যাবে।
যে ভুলগুলো আপনার সময় নষ্ট করে
প্রোডাকশন কোডে যে দুটি ভুল এখনও দেখা যায়, সেগুলো প্রতিরোধ করা সহজ।
প্রথমত, কনটেক্সটটি নিজেই এক্সপোর্ট করতে ভুলে যাওয়া। আপনি যদি কেবল Provider wrapper এক্সপোর্ট করেন এবং context অবজেক্টটিকে প্রাইভেট রাখেন, তবে একজন ডেভেলপার নতুন কোনো ফিচার লেখার সময় আপনার মডিউলটি রিফ্যাক্টর না করে useContext কল করতে পারবেন না। কনটেক্সটটি এক্সপোর্ট করুন যাতে ব্যবহারকারীরা (consumers) প্রোভাইডার এবং কনজিউমার হুক উভয়ই পরিষ্কারভাবে ইমপোর্ট করতে পারেন।
দ্বিতীয়ত, সংশ্লিষ্ট Provider-এর বাইরে useContext কল করা। যদি ThemeToggle ট্রি-এর এমন কোনো শাখায় রেন্ডার হয় যা ThemeProvider দ্বারা আবৃত (wrapped) নয়, তবে হুকটি createContext-এ পাস করা ডিফল্ট ভ্যালু রিটার্ন করবে, অথবা আপনি যদি কিছু পাস না করেন তবে undefined রিটার্ন করবে। এটি cannot read property of undefined-এর মতো নীরব ত্রুটির (silent failures) দিকে নিয়ে যায়। আপনি একটি যুক্তিসঙ্গত ডিফল্ট ভ্যালু নির্ধারণ করে বা হুক কলের শুরুতেই একটি স্পষ্ট এরর (error) থ্রো করে এটি প্রতিরোধ করতে পারেন।
Context বনাম Redux: সহজ রাখুন
আপনার সব সময় Redux-এর প্রয়োজন হয় না। মাঝারি আকারের প্রজেক্টের জন্য, useState বা useReducer-এর সাথে Context ব্যবহার করলেই স্টেট শেয়ারিংয়ের বেশিরভাগ কাজ হয়ে যায়। Redux তখনই কার্যকর হয় যখন আপনার time-travel debugging, জটিল middleware, বা এমন গ্লোবাল ট্রানজ্যাকশন প্রয়োজন হয় যা ক্রমানুসারে রোলব্যাক (roll back) করতে হবে। আপনার পুরো স্টেট যদি কেবল একটি user object, একটি theme string এবং একটি cart array হয়, তবে একটি স্টোর লাইব্রেরি কেবল অতিরিক্ত বয়লারপ্লেট (boilerplate) যোগ করবে যা আপনি কখনোই ব্যবহার করতে পারবেন না।
তা সত্ত্বেও, Context নিজে কোনো পূর্ণাঙ্গ স্টেট ম্যানেজমেন্ট সিস্টেম নয়। এটি আপনাকে একটি একক গ্লোবাল স্ন্যাপশট দেয় না এবং এটি সম্পর্কহীন কনটেক্সটগুলোর মধ্যে আপডেটগুলোকে ব্যাচ (batch) করে না। এটিকে prop drilling-এর বিকল্প হিসেবে ব্যবহার করুন, আপনার পুরো ডেটা লেয়ারের অপারেটিং সিস্টেম হিসেবে নয়।
মূল শিক্ষা
যে কম্পোনেন্টগুলোর কাছে প্রপস (props) প্রয়োজন নেই, তাদের মধ্য দিয়ে প্রপস পাস করা বন্ধ করুন। আপনার ট্রি-তে বিস্তৃত ডেটার জন্য নির্দিষ্ট (focused) কনটেক্সট তৈরি করুন, কনজিউমারদের কভার করার জন্য যথেষ্ট উচ্চস্তরে প্রোভাইডারগুলোকে র্যাপ (wrap) করুন, এবং যখন আপনি কোনো কালেকশন বা ফাংশন পাস করবেন তখন সর্বদা ভ্যালু অবজেক্টটিকে স্ট্যাবিলাইজ (stabilize) করুন। Context API আপনার React কোডকে সরাসরি রাখে: প্রপস লোকাল থাকে, গ্লোবাল ডেটা নির্বিঘ্নে প্রবাহিত হয় এবং আপনার কম্পোনেন্ট বাউন্ডারিগুলো পরিষ্কার থাকে।
