সরাসরি পর্বে যান
পর্ব ১৭ · ৯৩-এর মধ্যে · মডিউল ৩

Context আর useReducer — prop drilling কমানো

Context and useReducer — Reducing Prop Drilling

Taught in Bangla, this lesson explains prop drilling, the three parts of Context, the React 19 provider syntax, what Context is not and its real re-render cost with measured counts, splitting contexts and memoising values, useReducer as a pure ledger function, a Context plus reducer cart for Bazarghar, typed actions in TSX, testing reducers without React, and when to move to Zustand or Redux.

এই পর্বে যা শিখবেন

  • createContext, provider, useContext আর React 19-এর use ঠিকভাবে ব্যবহার করা
  • Context কেন state management নয়, আর কোন component কখন render হয় তা মেপে দেখা
  • Context ভাগ করা আর মান memoise করা
  • useReducer-এ এক click থেকে render পর্যন্ত পুরো পথ, আর reducer কেন pure হতে হবে
  • “বাজারঘর”-এর cart: typed action, useCart hook আর React ছাড়া reducer test

১ · Prop drilling — অনেক হাত ঘুরে আসা খবর

একটি পুরোনো সরকারি অফিস ভাবুন। বড় সাহেবের একটি চিঠি পিয়নের হাতে পৌঁছাতে হবে। চিঠি যায় উপ-পরিচালকের টেবিলে, সেখান থেকে সহকারীর টেবিলে, তারপর বড় বাবুর টেবিলে। মাঝের কেউ চিঠিটি পড়েন না, তবু প্রত্যেকের খাতায় সই লাগে। আর এই প্রথার জন্য সবাইকে চিঠির কথা জানতে হয়।

পর্ব ০৯-এ এই সমস্যার নাম জেনেছেন: prop drilling. মানটি থাকে ওপরে, দরকার হয় অনেক নিচে, আর মাঝের component শুধু props পার করে দেয়। “বাজারঘর”-এ App-এ cart আছে, আর হেডারের ছোট ব্যাজে দরকার মোট সংখ্যা। পথে থাকে Layout আর Header, যারা cart নিয়ে কিছুই করে না।

একই ট্রি, দুই পথ

Prop drilling vs Context

Prop drilling আর context-এর তুলনা Prop drilling Context App (count) Layout Header CartBadge App + provider Layout Header CartBadge
ছবি ১৭.১ — বাঁ দিকে মাঝের দুটি component-কে count নিতে হয়। ডান দিকে তারা কিছুই জানে না।

Context ধরার আগে দুটি কাজ করে দেখুন। React docs-ও একই কথা বলে। এক, শুধু কয়েক স্তর props পার হওয়া দোষের নয়। এতে কোন component কী ব্যবহার করে তা চোখে দেখা যায়। দুই, children দিয়ে composition ব্যবহার করুন। <Layout><CartBadge count={count} /></Layout> লিখলে Layout-কে আর count নিতে হয় না। এ দুটিতে না কুলোলে তবেই context.

তাহলে context কাদের জন্য? যে মান বহু দূরের বহু component-এ লাগে, আর কম বদলায়: ভাষা, theme, লগইন করা ব্যবহারকারী। State কোথায় রাখবেন, সেই সিদ্ধান্তের আলোচনা আছে পর্ব ১৬-এ। এখানে আমরা সেই সিদ্ধান্তের একটি হাতিয়ার ধরে দেখব।

২ · Context-এর তিন অংশ — রেডিও স্টেশনের মতো

রেডিও স্টেশন ভাবুন। স্টেশন একটি ফ্রিকোয়েন্সি ঠিক করে, যেমন ৯৮.৪। কেউ সম্প্রচার করে। আর যে শুনতে চায়, সে নিজের রেডিও সেই ফ্রিকোয়েন্সিতে ধরে। মাঝের বাড়িগুলো কিছুই জানে না। createContext হলো ফ্রিকোয়েন্সি, provider হলো সম্প্রচার, আর useContext হলো রেডিও ধরা।

১. তৈরি

createContext(default) component-এর বাইরে একবার ডাকা হয়। ফেরত আসে একটি context object.

ফ্রিকোয়েন্সি

২. মান দেওয়া

Provider: <ThemeContext value="dark">. এর নিচের পুরো subtree এই মান পায়।

সম্প্রচার

৩. পড়া

useContext(ThemeContext) সবচেয়ে কাছের ওপরের provider-এর মান ফেরত দেয়।

রেডিও

নিচের live block-এ আসল React 19 চলছে। Panel কোনো theme prop নেয় না, তবু ভেতরের Badge মান পায়। তিনটি অবস্থা দেখুন: provider নেই, একটি provider, আর provider-এর ভেতরে provider. “বদলান” বাটনও চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const ThemeContext = createContext('light'); // default মান

function Badge({ label }) {
  const theme = useContext(ThemeContext);
  console.log(label, '->', theme);
  return <p>{label}: <b>{theme}</b></p>;
}
function Panel({ label }) {   // Panel জানেই না theme কী
  return <Badge label={label} />;
}
function App() {
  const [theme, setTheme] = useState('dark');
  const flip = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'));
  return (
    <>
      <Panel label="no provider" />
      <ThemeContext value={theme}>
        <Panel label="outer" />
        <ThemeContext value="sepia"><Panel label="inner" /></ThemeContext>
      </ThemeContext>
      <button onClick={flip}>বদলান</button>
    </>
  );
}
createRoot(document.getElementById('root')).render(<App />);
</script>

আউটপুট: no provider -> light, outer -> dark, inner -> sepia. প্রথম লাইনে ওপরে কোনো provider নেই, তাই default মান। দ্বিতীয় লাইনে state-এর dark. তৃতীয় লাইনে সবচেয়ে কাছের provider জিতেছে। বাটন একবার চাপলে (Edge-এ চালিয়ে পাওয়া) আসে no provider -> light, outer -> light, inner -> sepia. বদলেছে শুধু outer; inner-এর সবচেয়ে কাছের provider এখনো sepia. তিনটি লাইনই আবার এসেছে, কারণ App নিজে render হয়েছে আর Panel-এ memo নেই।

Default মান — শেষ ভরসা, প্রধান পথ নয়

createContext-এর মান শুধু তখনই আসে, যখন ওপরে কোনো provider-ই নেই। Docs এটিকে “last resort” বলে, আর এটি কখনো বদলায় না। Provider থাকলে কিন্তু value না দিলে, default আসে না, আসে undefined. সত্যিকারের run-এ (React 19.3.1 development build, jsdom-এ) দেখুন:

<T>              -> value read: undefined
<T theme="dark"> -> value read: undefined
<T.Provider value="dark"> -> value read: dark
(provider নেই)    -> value read: light

console.error: The `value` prop is required for the `<Context.Provider>`.
Did you misspell it or forget to pass it?

তাই যে context-এর অর্থপূর্ণ default নেই, যেমন cart, সেখানে createContext(null) লিখুন। তাহলে পরে “provider ভুলে গেছি” ভুলটি স্পষ্ট error দিয়ে ধরা যায়। অংশ ৬-এ সেটি করব।

React 19-এ যা বদলেছে

কাজপুরোনো লেখাReact 19-এ
মান দেওয়া<C.Provider value><C value>
পড়াuseContext(C)useContext(C) বা use(C)
শর্তের ভেতরে পড়াকরা যায় নাuse(C) দিয়ে করা যায়

use hook নয়। Docs অনুযায়ী একে if বা লুপের ভেতরে ডাকা যায়, যা useContext-এ যায় না। তবে একে try/catch-এর ভেতরে ডাকা যাবে না। Provider খোঁজার নিয়ম দুটিতেই এক। শুধু একটি জায়গায় মান লাগলে এটি কাজের:

import { use } from 'react';

function Divider({ show }) {
  if (!show) return null;
  const theme = use(ThemeContext); // শর্তের পরে পড়া, use-এ চলে
  return <hr className={theme} />;
}

৩ · Context কী নয় — আর এর আসল খরচ

“Context ব্যবহার করে state management করছি।” এই বাক্যটি খুব শোনা যায়, কিন্তু কথাটি ঠিক নয়। Context নিজে কিছু রাখে না, কিছু বদলায় না। এটি শুধু একটি মান পৌঁছে দেয়। মান রাখছে useState বা useReducer. Redux-এর রক্ষণাবেক্ষণকারী Mark Erikson ২০২১ সালের এক লেখায় context-কে বলেছেন “transport mechanism”, আর একে dependency injection-এর একটি রূপ বলেছেন।

মূল ধারণা

Context-এ দুটি নিয়ম মনে রাখুন। এক: React value-এর আগের আর নতুন মান Object.is দিয়ে তুলনা করে। দুই: মান আলাদা হলে ওই context-এর প্রতিটি consumer আবার render হয়, সে মানের যে অংশই পড়ুক। memo এটি আটকায় না।

এবার ধাপে ধাপে দেখুন। ThemeProvider নিজের children পায় বাইরে থেকে। ট্রিতে দুজন consumer আছে: Badge আর ThemeButton. বাকিরা context পড়ে না।

ThemeProvideruseState + provider
Layoutmemo, consumer নয়
Footerconsumer নয়
Badgememo, consumer
ThemeButtonconsumer
Logoconsumer নয়
এই ধাপের অবস্থা
আগের value
নতুন value
Object.is
যারা render হলো
  1. শুরু। প্রথম render-এ সবাই একবার render হয়েছে। Provider-এর মান 'light'.
  2. State বদলাল। setTheme('dark') চলল, তাই ThemeProvider আবার render হলো। তার children বাইরে থেকে আসা একই element, তাই React সেগুলো সাধারণভাবে নতুন করে render করে না।
  3. তুলনা। Object.is('light', 'dark') মানে false. React বুঝল context-এর মান সত্যিই বদলেছে।
  4. Consumer খোঁজা। React provider-এর নিচে খুঁজে দেখে কে এই context পড়ে। Badge আর ThemeButton চিহ্নিত হলো। Layout, Footer, Logo পড়ে না, তাই বাদ।
  5. Render. দুজন consumer আবার render হলো। Badge-এর ওপরে memo আছে, তবু আটকায়নি। মাঝের Layout-এরও কিছু করতে হয়নি। Context পাশের দরজা দিয়ে consumer-এর কাছে যায়।
  6. ফাঁদ: inline object. Provider যদি value={{ theme }} লেখে, প্রতি render-এ নতুন object হয়। ভেতরের লেখা এক হলেও Object.is বলে false, তাই সব consumer শুধু শুধু render হয়। সত্যিকারের run-এ একই data-য় parent ৩ বার render হলে memo করা consumer-ও ৩ বার render হয়েছে।

এখান থেকে context-এর আসল সীমা বোঝা যায়। Provider যদি { count, add } একসাথে দেয়, আর কোনো component শুধু add পড়ে, তবু count বদলালে সে render হবে। Context-এ “শুধু এই অংশটুকু” বলার উপায় নেই। React 19.3.1-এর export-এ লেখার সময়ে useContextSelector জাতীয় কিছু নেই; আমরা export তালিকা দেখে নিশ্চিত হয়েছি।

Context কোথায় মানায়, কোথায় মানায় না

মানকত ঘন বদলায়Context কি ভালো পছন্দ?
Theme, ভাষাখুব কমহ্যাঁ
লগইন করা ব্যবহারকারীকমহ্যাঁ
ছোট শপের cartমাঝারি, click-এহ্যাঁ, state আর dispatch ভাগ করে
Input-এর লেখাপ্রতি অক্ষরেনা; local state রাখুন
মাউসের অবস্থান, scrollপ্রতি ফ্রেমেনা; ref বা store দেখুন
পণ্যের তালিকা (সার্ভারের ডেটা)সার্ভারে বদলায়না; server state টুল নিন

৪ · Context ভাগ করা আর মান memoise করা

সমাধান দুটি। ভাগ করা: যে মান একসাথে বদলায় না, তাদের আলাদা context-এ রাখা। Memoise করা: value object হলে useMemo দিয়ে তার reference স্থির রাখা। ফাংশন হলে useCallback. পর্ব ১৪-এ এদের দেখেছেন।

একই মাইকে বিদ্যুৎ অফিসের ঘোষণা আর বাজারের দরদাম প্রচার হলে যাঁর শুধু একটি দরকার, তাঁকেও সবটা শুনতে হয়। দুটি আলাদা ফ্রিকোয়েন্সি হলে যে যেটা চান, সেটাই ধরেন। Context ভাগ করা ঠিক এই কাজ।

নিচের block-এ একই পাতায় দুই সংস্করণ আছে। ওপরে এক context, নিচে ভাগ করা। দুই জায়গাতেই Btn শুধু লেখে, Show শুধু পড়ে। দুটি বাটনই চেপে লগ দেখুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const Big = createContext(null);
const Count = createContext(0);
const Add = createContext(null);
function BigProvider({ children }) {
  const [n, setN] = useState(0);
  const value = useMemo(() => ({ n, setN }), [n]);
  return <Big value={value}>{children}</Big>;
}
function SplitProvider({ children }) {
  const [n, setN] = useState(0);
  return <Add value={setN}><Count value={n}>{children}</Count></Add>;
}
const inc = x => x + 1;
const Btn1 = memo(() => { const { setN } = useContext(Big);
  console.log('one: Btn'); return <button onClick={() => setN(inc)}>এক +1</button>; });
const Show1 = memo(() => { const { n } = useContext(Big);
  console.log('one: Show'); return <b> {n} </b>; });
const Btn2 = memo(() => { const setN = useContext(Add);
  console.log('split: Btn'); return <button onClick={() => setN(inc)}>ভাগ +1</button>; });
const Show2 = memo(() => { const n = useContext(Count);
  console.log('split: Show'); return <b> {n} </b>; });
createRoot(document.getElementById('root')).render(<>
  <BigProvider><Btn1 /><Show1 /></BigProvider><hr />
  <SplitProvider><Btn2 /><Show2 /></SplitProvider>
</>);
</script>

লোডের সময় চারটি লাইন আসে: one: Btn, one: Show, split: Btn, split: Show. এবার “এক +1” চাপলে (Edge-এ চালিয়ে পাওয়া) আসে one: Btn আর one: Show. অর্থাৎ Btn-ও render হয়েছে, যদিও সে সংখ্যা দেখায় না। “ভাগ +1” চাপলে আসে শুধু split: Show. setN React-এর স্থির function, তাই Add context কখনো বদলায় না। লক্ষ করুন, প্রথম সংস্করণে useMemo আর memo দুটোই আছে, তবু Btn বাঁচেনি। সংখ্যা বদলালে useMemo-ও নতুন object দেয়।

এক contextএকটি value-তে সব
CartContext ─ value = { count, add }
   │
   ├─ Header (count পড়ে)
   └─ ProductCard × 20 (add পড়ে)
ভাগ করাদুটি আলাদা context
CartCountContext ─ value = count
   └─ Header
CartActionsContext ─ value = add (স্থির)
   └─ ProductCard × 20
Render-এর হিসাব
count
Object.is (count-এর context)
Object.is (add-এর context)
Header render
ProductCard render
মোট
  1. এক context. হেডারে একটি সংখ্যা, আর ২০টি কার্ডে “কার্টে যোগ করুন” বাটন। সবাই একই CartContext পড়ে। বাটনের দরকার শুধু add.
  2. এক click. count হলো ১, তাই value নতুন object. ২১ জন consumer-ই render হলো, যদিও ২০টি কার্ডের স্ক্রিনে কিছুই বদলায়নি।
  3. ভাগ করা হলো। যা বদলায় (count) আর যা বদলায় না (add) এখন দুটি আলাদা context-এ।
  4. এক click. count-এর context বদলাল, Header render হলো। add একই reference, Object.is সত্য, তাই ২০টি কার্ড বসে থাকল। মোট ১টি render.
  5. ১০ click. সত্যিকারের run-এর ফল: এক context-এ ২১০, ভাগ করায় ১০।

মেপে দেখা হিসাব

React 19.3.1 development build-এ ২০টি memo করা কার্ড, ১টি হেডার আর ১০ বার click দিয়ে গোনা। এখানে সময় নয়, render-সংখ্যা গোনা হয়েছে, তাই অন্য কম্পিউটারেও একই সংখ্যা আসার কথা।

after 10 clicks one-context {"Header":10,"Card":200}
after 10 clicks split       {"Header":10}
inline object, same data, 3 parent renders: {"Reader":3}
হিসাব

Consumer-সংখ্যা C, click-সংখ্যা k হলে এক context-এ render = C × k = ২১ × ১০ = ২১০. ভাগ করলে শুধু হেডার: ১ × ১০ = ১০. কমেছে (২১০ − ১০) ÷ ২১০ = ৯৫.২%. এবার সময় অনুমান করুন। ধরে নিন একটি কার্ড render হতে ০.৫ ms লাগে। তাহলে এক click-এ ২০ × ০.৫ = ১০ ms, যা ৬০ fps-এর ১৬.৭ ms ফ্রেম বাজেটের অর্ধেকের বেশি। ০.৫ ms শুধু অনুমান; নিজের অ্যাপে Profiler দিয়ে মাপুন।

কিন্তু সবসময় ভাগ করবেন না। Theme বছরে কয়েকবার বদলালে ২১টি render কোনো সমস্যা নয়। আর React Compiler চালু থাকলে inline object অনেক ক্ষেত্রে নিজেই স্থির হতে পারে, তবে ভাগ করার কাজটি সে করে দেয় না। আগে মাপুন, তারপর ভাগ করুন।

Value memoise করা — কখন, কীভাবে

কখনো মান ভাগ করা যায় না, কারণ user আর login একসাথেই দরকার। তখন object-টি স্থির রাখুন। নিচের কোডটি React docs-এর নকশা অনুসরণ করে। useMemo ছাড়া AuthProvider-এর parent render হলেই সব consumer render হতো। এখন শুধু user বদলালে হবে।

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const logout = useCallback(() => setUser(null), []);
  const value = useMemo(() => ({ user, logout }), [user, logout]);

  return <AuthContext value={value}>{children}</AuthContext>;
}

মান যদি সংখ্যা বা লেখার মতো primitive হয়, useMemo লাগে না; Object.is সরাসরি মান মেলায়। আর এখানে প্রমাণ বা token কীভাবে রাখবেন, সেটি context-এর বিষয় নয়। তার পথ দেখবেন পর্ব ৪৫ আর পর্ব ৪৬-এ।

Children pattern — Provider-কে আলাদা component করুন

উপরের সব উদাহরণে provider একটি আলাদা component, যে children নেয়। এটি শুধু গোছানোর জন্য নয়। App-এর ভেতরেই state রেখে provider লিখলে state বদলালে App render হবে, তার সাথে তার JSX-এর সব child-ও। আলাদা CartProvider-এ children আসে বাইরে থেকে, তাই state বদলালেও সেই element একই থাকে। ফলে শুধু consumer render হয়। মাপা run-এও তাই দেখা গেছে: context না পড়া component ১০ click-এ একবারও render হয়নি।

আরেকটি কথা। Provider যত নিচে বসানো যায়, তত ভালো। Cart শুধু দোকানের অংশে লাগলে admin-এর পাতায় CartProvider রাখার দরকার নেই। এতে consumer-এর সংখ্যা কমে, আর কোডের সীমানাও স্পষ্ট হয়। সব provider main.jsx-এ জমা করা একটি সাধারণ অভ্যাস, কিন্তু সেটি জরুরি নয়।

৫ · useReducer — হিসাব খাতার নিয়ম

এবার অন্য একটি সমস্যা। cart-এ যোগ, কমানো, মুছে ফেলা, খালি করা, সব জায়গায় আলাদা setItems ছড়িয়ে থাকলে একদিন একই নিয়ম দুই জায়গায় দুইভাবে লেখা হবে। যেমন এক জায়গায় qty ০ হলে লাইন মোছে, অন্য জায়গায় মোছে না।

রেস্তোরাঁয় ওয়েটার রান্নাঘরে ঢুকে নিজে রান্না করেন না। তিনি একটি স্লিপ দেন: “টেবিল ৫, এক প্লেট কাচ্চি।” রাঁধুনি স্লিপ দেখে নিজের নিয়মে কাজ করেন। স্লিপ হলো action, স্লিপ পাঠানো হলো dispatch, আর রাঁধুনির নিয়ম হলো reducer. কী ঘটেছে তা ওয়েটার বলেন, কীভাবে বদলাবে তা রাঁধুনি ঠিক করেন।

গঠনটি ছোট। useReducer(reducer, initialState) ফেরত দেয় [state, dispatch]. Reducer-এর চেহারা সবসময় এক: (state, action) => নতুন state. Array-র reduce-এ যে function দেন, সেটিও একই আকারের, নামটিও সেখান থেকে। Action সাধারণত একটি object, যেখানে type বলে কী ঘটেছে। নাম দিন ঘটনার মতো: 'added', 'removed', setItems-এর মতো আদেশ নয়।

এখন এক click-এর পুরো পথ দেখুন। ধরুন cart খালি, আর ব্যবহারকারী “মসুর ডাল” যোগ করছেন। খেয়াল করুন reducer ঠিক কখন চলে।

1function Shop() {2  const [state, dispatch] = useReducer(cartReducer, { items: [] });3  function handleClick() {4    dispatch({ type: 'added', item: dal });5    console.log(state.items.length);6  }7  return <button onClick={handleClick}>কার্ট: {state.items.length}</button>;8}9function cartReducer(state, action) {10  if (action.type === 'added') return { items: [...state.items, action.item] };11  return state;12}
React-এর ভেতরে
state.items.length
action queue
স্ক্রিনে
  1. শান্ত অবস্থা। আগের render থেকে স্ক্রিনে “কার্ট: 0”. state এই render-এর snapshot.
  2. Click. ব্যবহারকারী বাটন চাপলেন। React handleClick চালাল।
  3. Dispatch. Action সারিতে (queue) জমা হলো আর React একটি render ঠিক করে রাখল। Reducer এখনো চলেনি। dispatch সঙ্গে সঙ্গে ফিরে এলো।
  4. পরের লাইন। console.log ছাপল 0. চলতি কোডে state বদলায় না। এটি পর্ব ১০-এর snapshot-এর একই নিয়ম।
  5. Render শুরু। Handler শেষ, React batching সেরে Shop আবার ডাকল। useReducer লাইনে এসে সারির action হাতে নিল।
  6. Reducer চলল। Render-এর ভেতরে cartReducer({ items: [] }, action) চলে ফেরত দিল ১টি item-এর নতুন object. সারি খালি।
  7. তুলনা আর JSX. Object.is বলল state আলাদা, তাই নতুন JSX তৈরি হলো: “কার্ট: 1”. React আগের ট্রির সাথে diff করল।
  8. Commit আর paint. DOM-এ শুধু লেখাটি বদলাল, ব্রাউজার আঁকল। স্ক্রিনে “কার্ট: 1”. চক্র শেষ।

এই ক্রম মুখের কথা নয়। React 19.3.1-এর development build-এ লগ বসিয়ে সত্যিকারের run-এর আউটপুট (StrictMode ছাড়া):

== click
dispatch-এর আগে count = 0
dispatch-এর পরে count = 0
Cart render শুরু
  reducer চলছে {"state":{"count":0},"action":{"type":"add"}}
Cart render শেষ, count = 1
Child render
== same state (noop)
Cart render শুরু
  reducer চলছে {"state":{"count":1},"action":{"type":"noop"}}
Cart render শেষ, count = 1
== two dispatches in one tick
Cart render শুরু
  reducer চলছে {"state":{"count":1},"action":{"type":"add"}}
  reducer চলছে {"state":{"count":2},"action":{"type":"add"}}
Cart render শেষ, count = 3
Child render

তিনটি জিনিস লক্ষ করুন। Reducer চলেছে render-এর ভেতরে। Reducer একই state ফেরত দিলে Cart একবার চলেছে, কিন্তু Child render হয়নি; docs-এর “skip” কথাটি মূলত children-এর জন্য। আর এক tick-এ দুটি dispatch হলে একটিই render, reducer দুবার। সব action সারি ধরে চলে, তাই পরেরটি আগের ফল পায়।

Reducer কেন pure হতে হবে

Pure function মানে একই ইনপুটে একই ফল, আর বাইরের কিছু না বদলানো। Reducer render-এর ভেতরে চলে, আর render যেকোনো সংখ্যকবার চলতে পারে। তাই ভেতরে fetch, localStorage.setItem, Math.random() বা Date.now() রাখা যাবে না। এলোমেলো id বা সময় লাগলে সেটি event handler-এ তৈরি করে action-এ পাঠান।

StrictMode development-এ reducer দুবার ডেকে এই ভুল ধরিয়ে দেয়, আর একটি ফল ফেলে দেয়। সত্যিকারের run-এ এক dispatch-এ পুরোনো state বদলানো (state.items.push) আর ঠিক reducer পাশাপাশি:

impure reducer calls: 2 items: ["dal","dal"]
pure   reducer calls: 2 items: ["dal"]

এক click-এ দুটি ডাল! এটি production-এ ধরা পড়ত না, আর react-lab.js-এর live block-এও দেখবেন না, কারণ সেটি production build. Development-এ দেখলে StrictMode-কে দোষ দেবেন না, reducer ঠিক করুন।

শুরুর state — তৃতীয় argument

পুরো স্বাক্ষরটি হলো useReducer(reducer, initialArg, init?). init দিলে শুরুর state হয় init(initialArg). শুরুর মান তৈরি করা ভারী হলে এটি কাজে লাগে, কারণ React একে শুধু শুরুতে ডাকে। Function-টি নিজে পাঠান, তার ফল নয়। StrictMode-এ reducer-এর মতো init-ও দুবার চলে, তাই এতে কিছু লেখা বা পাঠানো চলবে না।

function emptyCart(owner) {
  return { owner, items: [] };
}
// ঠিক: React দরকারমতো একবার ডাকবে
const [state, dispatch] = useReducer(cartReducer, 'guest', emptyCart);

আর পরের state কী হবে তা handler-এই জানতে চাইলে reducer-কে নিজেই ডাকতে পারেন: const next = cartReducer(state, action);. Docs-ও এই পথ দেখায়। Reducer সাধারণ function বলেই এটি সম্ভব।

useState না useReducer?

যা দেখছেনuseStateuseReducer
State-এর আকারএকটি সংখ্যা, লেখা, booleanকয়েকটি মান, যারা একসাথে বদলায়
বদলানোর উপায়দু-একটি সহজ পথঅনেক ধরনের ঘটনা
Logic কোথায়Event handler-এ ছড়ানোএক function-এ এক জায়গায়
TestComponent render করেসাধারণ function ডেকে
Debugকে বদলাল খুঁজতে হয়Action লগ করলেই পুরো ইতিহাস
বাড়তি লেখাকমবেশি

একটি toggle-এ useReducer শুধু বাড়তি লেখা। আবার useReducer মানেই ভালো কোড নয়; এটি রুচি ও আকারের প্রশ্ন, আর React docs দুটিকেই ঠিক বলে। একটি ভালো সংকেত: setA, setB, setC একসাথে ডাকতে হচ্ছে, আর ভুলে একটি বাদ পড়লে অবস্থা অসঙ্গত হয়। তখন reducer নিন।

৬ · Context + reducer — বাজারঘরের cart

এবার দুটি জোড়া দিই। Reducer জানে কীভাবে state বদলাবে। Context জানে কীভাবে দূরে পৌঁছাবে। দুটি মিলিয়ে নিচের গঠনটি React docs-এর “Scaling Up with Reducer and Context” পরামর্শ অনুযায়ী লেখা। state আর dispatch আলাদা context-এ থাকে, কারণ docs অনুযায়ী dispatch-এর পরিচয় স্থির। অংশ ৪-এর লাভ তাই বিনা খরচে মেলে।

নিচের live block-এ পুরো cart আছে। দুটি পণ্যের বাটন চেপে দেখুন। কোন component-এর লগ আসে, আর কারটি আসে না।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function cartReducer(state, action) {
  switch (action.type) {
    case 'added': {
      if (!state.items.some(i => i.id === action.item.id))
        return { items: [...state.items, { ...action.item, qty: 1 }] };
      return { items: state.items.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i) };
    }
    case 'cleared': return { items: [] };
    default: throw new Error('অজানা action: ' + action.type);
  }
}
const StateCtx = createContext(null), DispatchCtx = createContext(null);
function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return <DispatchCtx value={dispatch}><StateCtx value={state}>{children}</StateCtx></DispatchCtx>;
}
function useCart() {
  const { items } = useContext(StateCtx);
  return { count: items.reduce((n, i) => n + i.qty, 0), total: items.reduce((s, i) => s + i.price * i.qty, 0) };
}
const Header = () => { const { count, total } = useCart(); console.log('Header', count, total);
  return <h3>🛒 {count}টি · ৳{total}</h3>; };
const Add = memo(({ item }) => { const dispatch = useContext(DispatchCtx); console.log('Add', item.name);
  return <button onClick={() => dispatch({ type: 'added', item })}>{item.name} ৳{item.price}</button>; });
const dal = { id: 1, name: 'ডাল', price: 140 }, tel = { id: 2, name: 'তেল', price: 190 };
createRoot(document.getElementById('root')).render(<CartProvider><Header /><Add item={dal} /><Add item={tel} /></CartProvider>);
</script>

লোডে আসে Header 0 0, Add ডাল, Add তেল. এরপর ডাল, ডাল, তেল চাপলে (Edge-এ চালিয়ে পাওয়া) শুধু আসে Header 1 140, Header 2 280, Header 3 470. দুই বার ডাল আর এক বার তেল: ২ × ১৪০ + ১ × ১৯০ = ৪৭০, মিলে গেছে। Add আর লগ দেয়নি, কারণ সে শুধু dispatch পড়ে। মূল্যগুলো কাল্পনিক। Header-এর count আর total state-এ রাখা হয়নি, হিসাব করে বের করা হয়েছে। এটিই derived state, এতে দুই জায়গায় মান অমিলের ভয় নেই।

useCart কেন আলাদা hook

Component যদি সরাসরি useContext(StateCtx) লেখে, তিনটি সমস্যা হয়। Context object সবাইকে জানাতে হয়। Provider ভুলে গেলে null-এর ওপর ত্রুটি আসে, যার অর্থ বোঝা কঠিন। আর হিসাবের কোড প্রতিটি component-এ ঘোরে। একটি custom hook তিনটিই সমাধান করে। ভেতরে কী আছে তা বাকি অ্যাপ জানে না, তাই পরে Zustand-এ গেলে শুধু এই ফাইলটি বদলাবে।

সেরা অভ্যাস হলো context object export না করা। ফাইল থেকে শুধু CartProvider, useCart আর useCartDispatch বের হবে। তাহলে Provider ছাড়া ব্যবহারের ভুলের বার্তা সত্যিকারের run-এ এমন আসে: useCart must be used inside <CartProvider>. এক নজরেই সমস্যা বোঝা যায়।

Cart সেভ করবেন কোথায়

Reducer-এ cart-এর localStorage-এ সেভ করার জায়গা নেই। সেটি হবে CartProvider-এর একটি effect-এ, যা state বদলালে চলে। পর্ব ১৩ মনে করুন। পড়ার কাজটি হবে init-এ। তবে localStorage পড়া মানে বাইরের জগৎ পড়া, আর সেখানকার ডেটা নষ্টও হতে পারে। তাই পড়ার সময় try রাখুন, আর ভুল পেলে খালি cart দিন।

function loadCart() {
  try {
    const raw = localStorage.getItem('bazarghar-cart');
    return raw ? JSON.parse(raw) : { items: [] };
  } catch {
    return { items: [] };   // নষ্ট ডেটা বা ব্লক করা storage
  }
}

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, undefined, loadCart);

  useEffect(() => {
    localStorage.setItem('bazarghar-cart', JSON.stringify(state));
  }, [state]);
  // … provider দুটি আগের মতোই
}

Live block-এর sandbox-এ localStorage বন্ধ থাকতে পারে, তাই এটি jsdom-এর আসল localStorage-এ চালানো হয়েছে। সত্যিকারের run-এ remount-এর পরে cart ফিরে এলো, আর নষ্ট ডেটায় খালি cart পাওয়া গেল:

after add   : {"items":[{"id":1,"qty":2}]}
after reload: {"items":[{"id":1,"qty":2}]}
corrupt data: {"items":[]}

Next.js-এ সার্ভারে localStorage নেই, তাই সেখানে এই পথ বদলাতে হবে; পর্ব ৫৬-এ hydration দেখবেন। Cart-এ শুধু পণ্যের id আর qty রাখাই নিরাপদ। দাম ব্রাউজার থেকে বিশ্বাস করবেন না, অর্ডারের সময় সার্ভারে আবার হিসাব করুন। আর সার্ভারে রাখা cart হলে সেটি server state, যার পথ আছে পর্ব ১৯-এ।

৭ · TSX-এ typed action — ভুল action ধরে দেওয়া

JavaScript-এ dispatch({ type: 'add' }) লিখলে, যেখানে 'added' হওয়ার কথা, ভুলটি ধরা পড়বে শুধু চালানোর সময়। TypeScript-এর discriminated union এই ভুল লেখার সময়েই ধরে। পর্ব ০৬-এ এটি দেখেছেন। এবার কাজে লাগাই। type ঘরটি দেখে TypeScript বোঝে কোন action-এ আর কোন ঘর আছে।

export type CartItem = { id: number; name: string; price: number; qty: number };
export type CartState = { items: CartItem[] };

export type CartAction =
  | { type: "added"; item: Omit<CartItem, "qty"> }
  | { type: "qty_set"; id: number; qty: number }
  | { type: "removed"; id: number }
  | { type: "cleared" };

function assertNever(x: never): never {
  throw new Error("অজানা action: " + JSON.stringify(x));
}

export function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case "added": {
      const found = state.items.find((i) => i.id === action.item.id);
      if (found) {
        return {
          items: state.items.map((i) =>
            i.id === found.id ? { ...i, qty: i.qty + 1 } : i,
          ),
        };
      }
      return { items: [...state.items, { ...action.item, qty: 1 }] };
    }
    case "qty_set":
      if (action.qty <= 0) {
        return { items: state.items.filter((i) => i.id !== action.id) };
      }
      return {
        items: state.items.map((i) =>
          i.id === action.id ? { ...i, qty: action.qty } : i,
        ),
      };
    case "removed":
      return { items: state.items.filter((i) => i.id !== action.id) };
    case "cleared":
      return { items: [] };
    default:
      return assertNever(action);
  }
}

export const cartTotal = (s: CartState) =>
  s.items.reduce((sum, i) => sum + i.price * i.qty, 0);

এবার ভুলগুলো ইচ্ছা করে লিখে tsc চালাই। সত্যিকারের run, TypeScript 6.0.3 (লেখার সময়ে), strict চালু:

$ npx tsc -p .
src/bad.ts(3,18): error TS2322: Type '"add"' is not assignable to type
  '"added" | "qty_set" | "removed" | "cleared"'.
src/bad.ts(4,16): error TS2345: Argument of type '{ type: "qty_set"; id: number; }'
  is not assignable to parameter of type 'CartAction'.
  Property 'qty' is missing in type '{ type: "qty_set"; id: number; }' but required
  in type '{ type: "qty_set"; id: number; qty: number; }'.

প্রথমটিতে বানান ভুল, দ্বিতীয়টিতে qty নেই। দুটিই চালানোর আগেই ধরা পড়ল। আরও ভালো হলো assertNever-এর কাজ, যা একটি exhaustive check. default-এ পৌঁছে action-এর type হয় never, কারণ সব case শেষ। এখন ধরুন কেউ case "cleared" মুছে দিলেন, বা নতুন action যোগ করে case লিখতে ভুলে গেলেন:

$ npx tsc -p tsconfig.miss.json
src2/cartReducer.ts(39,26): error TS2345: Argument of type '{ type: "cleared"; }'
  is not assignable to parameter of type 'never'.

Compiler নাম ধরে বলে দিল কোন action বাদ পড়েছে। বড় টিমে নতুন action যোগ করলে এই একটি লাইনই reducer-এর সব ফাঁক দেখিয়ে দেয়। Provider-এর typed রূপটি নিচে; এটিও tsc-এ শূন্য error দিয়ে পাস করেছে। use নেওয়া হয়নি, কারণ hook-এর ওপরের স্তরে পড়ছি, যেখানে useContext-ই যথেষ্ট।

import { createContext, useContext, useReducer } from "react";
import type { Dispatch, ReactNode } from "react";
import { cartReducer, cartTotal } from "./cartReducer";
import type { CartAction, CartState } from "./cartReducer";

const CartStateContext = createContext<CartState | null>(null);
const CartDispatchContext = createContext<Dispatch<CartAction> | null>(null);

export function CartProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return (
    <CartDispatchContext value={dispatch}>
      <CartStateContext value={state}>{children}</CartStateContext>
    </CartDispatchContext>
  );
}

export function useCart() {
  const state = useContext(CartStateContext);
  if (state === null) throw new Error("useCart must be used inside <CartProvider>");
  const count = state.items.reduce((n, i) => n + i.qty, 0);
  return { items: state.items, count, total: cartTotal(state) };
}

export function useCartDispatch() {
  const dispatch = useContext(CartDispatchContext);
  if (dispatch === null) throw new Error("useCartDispatch must be used inside <CartProvider>");
  return dispatch;
}

if (state === null) লাইনের পরে TypeScript জানে state আর null নয়। এটি narrowing. তাই ব্যবহারকারী component-কে state?.items লিখতে হয় না।

৮ · Reducer test — React ছাড়াই

Reducer-এর সবচেয়ে সুন্দর দিক এটি। সে একটি সাধারণ function. Browser লাগে না, DOM লাগে না, React-ও লাগে না। ইনপুট দিন, ফল মেলান। দাম, qty আর মুছে ফেলার সব নিয়ম এক ঝটকায় যাচাই হয়ে যায়। নিচের block-এ কোনো test library নেই, শুধু হাতে লেখা assertion. শেষ test-টি ইচ্ছা করেই একটি ভুল reducer ধরছে।

'use strict';
function cartReducer(state, action) {
  if (action.type === 'added') {
    const has = state.items.some(i => i.id === action.item.id);
    if (!has) return { items: [...state.items, { ...action.item, qty: 1 }] };
    return { items: state.items.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i) };
  }
  if (action.type === 'qty_set') {
    if (action.qty <= 0) return { items: state.items.filter(i => i.id !== action.id) };
    return { items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) };
  }
  throw new Error('unknown action: ' + action.type);
}
function badReducer(state, action) { state.items.push(action.item); return state; } // ভুল
function test(name, fn) {
  try { fn(); console.log('PASS', name); }
  catch (e) { console.log('FAIL', name, '-', e.message); }
}
function same(a, b) { const x = JSON.stringify(a), y = JSON.stringify(b);
  if (x !== y) throw new Error(x + ' != ' + y); }
const dal = { id: 1, name: 'dal', price: 140 };
const frozen = () => Object.freeze({ items: Object.freeze([]) });
test('new item gets qty 1', () => same(
  cartReducer(frozen(), { type: 'added', item: dal }).items[0].qty, 1));
test('same item twice: qty 2, one line', () => {
  const s1 = cartReducer(frozen(), { type: 'added', item: dal });
  const s2 = cartReducer(s1, { type: 'added', item: dal });
  same([s2.items.length, s2.items[0].qty], [1, 2]);
});
test('qty 0 removes the line', () => {
  const s1 = cartReducer(frozen(), { type: 'added', item: dal });
  same(cartReducer(s1, { type: 'qty_set', id: 1, qty: 0 }).items, []);
});
test('bad reducer on frozen state', () => badReducer(frozen(), { item: dal }));

আউটপুট: PASS new item gets qty 1, PASS same item twice: qty 2, one line, PASS qty 0 removes the line, FAIL bad reducer on frozen state - Cannot add property 0, object is not extensible. প্রথম তিনটি পাস। শেষটি ফেল করেছে, আর এটিই চাওয়া ছিল। Object.freeze করা state-এ push করতে গেলে strict mode-এ error হয়। অর্থাৎ state-কে freeze করে test চালালে reducer পুরোনো state বদলাচ্ছে কি না ধরা পড়ে। লক্ষ করুন, ভালো reducer তিনটি test-এই frozen state নিয়ে পাস করেছে।

বাস্তব প্রজেক্টে একই কাজ করে Vitest. উপরের TypeScript reducer-এর জন্য লেখা একটি test, আর সত্যিকারের run (Vitest 5.0.5, React 19.3.1, Node 24, লেখার সময়ে):

import { describe, expect, it } from "vitest";
import { cartReducer, cartTotal, type CartState } from "./cartReducer";

const dal = { id: 1, name: "মসুর ডাল", price: 140 };
const tel = { id: 2, name: "সয়াবিন তেল", price: 190 };
const empty: CartState = { items: [] };

describe("cartReducer", () => {
  it("মোট হিসাব: 2 x 140 + 1 x 190 = 470", () => {
    let s = cartReducer(empty, { type: "added", item: dal });
    s = cartReducer(s, { type: "added", item: dal });
    s = cartReducer(s, { type: "added", item: tel });
    expect(cartTotal(s)).toBe(470);
  });
  // … আরও চারটি it(): নতুন পণ্য, একই পণ্য, qty 0, পুরোনো state না বদলানো
});
$ npx vitest run --reporter=verbose src/cartReducer.test.ts

 RUN  v5.0.5 ~/bazarghar

 ✓ src/cartReducer.test.ts > cartReducer > নতুন পণ্য যোগ করলে qty হয় 1 1ms
 ✓ src/cartReducer.test.ts > cartReducer > একই পণ্য আবার যোগ করলে qty বাড়ে, নতুন লাইন হয় না 1ms
 ✓ src/cartReducer.test.ts > cartReducer > qty 0 হলে লাইনটি মুছে যায় 0ms
 ✓ src/cartReducer.test.ts > cartReducer > পুরোনো state বদলায় না 0ms
 ✓ src/cartReducer.test.ts > cartReducer > মোট হিসাব: 2 x 140 + 1 x 190 = 470 0ms

 Test Files  1 passed (1)
      Tests  5 passed (5)

Component test লাগলেও reducer test আগে করুন, কারণ এটি দ্রুত আর স্থির। Unit test-এর তত্ত্ব আছে Software Engineering কোর্সে, Vitest-এর পুরো ব্যবহার পর্ব ৭৩-এ, আর component test পর্ব ৭৪-এ।

৯ · কখন Zustand বা Redux-এ যাবেন

Context + reducer অনেক ছোট ও মাঝারি অ্যাপে যথেষ্ট, আর কোনো বাড়তি dependency লাগে না। তবে এর একটি সীমা স্থায়ী: cart বদলালে useCart ব্যবহার করা সবাই render হয়, যে শুধু count চায় সেও। Store library-গুলো এখানে অন্যভাবে কাজ করে। State থাকে React-এর বাইরে, useSyncExternalStore-এর মতো পথে component সাবস্ক্রাইব করে, আর selector দিয়ে শুধু দরকারি অংশ বেছে নেয়।

যে সংকেত দেখছেনযা ভাবুন
মান কম বদলায়, অল্প জায়গায় লাগে (theme, ভাষা, user)Context
একটি feature-এর ভেতরে জটিল নিয়মuseReducer, দরকারে context-সহ
ঘন ঘন বদলায়, বহু consumer অংশবিশেষ পড়েSelector-সহ store: Zustand বা Redux Toolkit
React-এর বাইরে থেকেও state পড়তে বা বদলাতে হয়Store
Action-এর ইতিহাস, time-travel debugging, বড় টিমের কড়া নিয়মRedux Toolkit
ডেটা আসলে সার্ভারের (পণ্যের তালিকা, order)এর কোনোটিই নয়: পর্ব ১৯-এর server state টুল

লেখার সময়ে npm-এ zustand 5.0.16 আর @reduxjs/toolkit 2.13.0. কোনটি “সেরা” তা নিয়ে সমাজে মতভেদ আছে। অনেক টিম Zustand-এর সরলতা পছন্দ করে, অনেক বড় টিম Redux-এর কাঠামো ও টুল চায়, আর কেউ বলেন বেশিরভাগ অ্যাপে context-ই যথেষ্ট। যাওয়ার আগে একটি প্রশ্ন করুন: সমস্যাটি কি মাপা হয়েছে? useCart hook-এর পেছনে সব লুকানো থাকলে পরে বদলানো সস্তা, তাই আগেভাগে library আনার দরকার নেই। দুটির বিস্তারিত পাবেন পর্ব ১৮ · Zustand আর Redux Toolkit-এ।

React-এর প্রথম দিকে একটি পরীক্ষামূলক context ছিল, যা docs-এও এড়াতে বলা হতো। বর্তমান createContext এসেছে React 16.3-এ (২০১৮)। useReducer আর useContext এসেছে React 16.8-এর hook-এর সাথে (২০১৯)। আর <Context>-কে সরাসরি provider হিসেবে লেখা শুরু হয়েছে React 19-এ (২০২৪)। Redux এসেছিল ২০১৫ সালে, Dan Abramov আর Andrew Clark-এর হাতে, Flux আর Elm-এর ধারণা থেকে।

আরও গভীরে: React প্রতিটি component-এর Fiber-এ সে কোন কোন context পড়েছে তা লিখে রাখে। মান বদলালে provider-এর নিচে এই তালিকা দেখেই consumer চিহ্নিত হয়, তাই memo-র দেওয়াল কাজে আসে না। বাইরের store-এর ক্ষেত্রে আলাদা ঝুঁকি আছে: concurrent rendering-এর মাঝে store বদলালে একই স্ক্রিনে দুটি ভিন্ন মান দেখা যেতে পারে, যাকে বলে tearing. useSyncExternalStore এটি ঠেকাতেই তৈরি। Context-এর মান React-এর নিজের render চক্রেই আসে।

পরীক্ষা ও ইন্টারভিউতে:

  • Context কি Redux-এর বদলি? না। Context শুধু মান পৌঁছায়; selector, middleware, devtools কিছু দেয় না।
  • memo করা child কেন render হলো? সে context পড়ে, আর context-এর মান বদলেছে; memo শুধু props দেখে।
  • Context-এ re-render কমানোর উপায়? Context ভাগ করা, value memoise করা, state ও dispatch আলাদা রাখা, children pattern.
  • dispatch-এর পরেই state পড়লে কী আসে? পুরোনো মান, কারণ state বদলায় পরের render-এ।
  • Reducer pure কেন? এটি render-এ চলে, একাধিকবার চলতে পারে, আর StrictMode development-এ দুবার ডাকে।

১০ · AI-এর যুগে Context আর reducer

“একটি cart বানিয়ে দাও” বললে AI সহজেই চলনসই কোড দেবে। কোড চলবেও। সমস্যা ধরা পড়ে পরে, যখন অ্যাপ বড় হয়। AI আপনার অ্যাপের render-সংখ্যা মাপে না, তাই এই কাজ আপনার।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
value={{ state, dispatch }} inline লেখেপ্রতি render-এ নতুন object, সব consumer renderReact DevTools-এর Profiler
একটি AppContext-এ সবকিছুএকটি অক্ষর টাইপেও পুরো অ্যাপ renderRender লগ, context ভাগ
Reducer-এ localStorage বা fetchStrictMode-এ দুবার চলে, reducer আর pure নয়কোড পড়া, freeze test
state.items.push(...)এক click-এ দুটি পণ্য, বা স্ক্রিন বদলায় নাFrozen state দিয়ে test
default: return stateভুল action চুপচাপ হারিয়ে যায়assertNever বা throw
পুরোনো .Provider লেখেচলে, তবে React 19 docs একে legacy বলেনিজের version-এর docs দেখা
অকারণে store library যোগ করেবাড়তি dependency, বাড়তি শেখাpackage.json-এর diff পড়া
Token বা গোপন মান context-এ রাখেClient-এর সব মান ব্রাউজারে দেখা যায়; context কোনো নিরাপত্তা নয়Secret কখনো client-এ নয়; Session ও token দেখুন

AI-কে কাজ দেওয়ার সময় version আর সীমা স্পষ্ট বলুন। তাহলে সে নিজের ধারণায় নতুন library আনার সুযোগ কম পায়:

আমার প্রজেক্ট React 19, TypeScript strict, কোনো state library নেই। অনলাইন শপের cart-এর জন্য useReducer আর Context দিয়ে কোড লেখো। শর্ত: ১. React 19-এর লেখা ব্যবহার করো: <CartContext value={...}>, .Provider নয়। ২. state আর dispatch আলাদা context-এ রাখো। context object export করবে না, শুধু CartProvider, useCart, useCartDispatch. ৩. action হবে discriminated union, default-এ never দিয়ে exhaustive check. ৪. reducer pure থাকবে: localStorage, fetch, Date.now, Math.random ভেতরে নয়। ৫. নতুন package যোগ করবে না। ৬. Vitest-এ reducer-এর test দাও, state Object.freeze করে। শেষে বলো, কোন component কোন action-এ re-render হবে, আর কোন API-এর জন্য react.dev-এর কোন পাতা দেখা উচিত।

AI-এর দেওয়া context কোড হাতে পেলে

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১“Context + useReducer = Redux”, এই কথাটি কতটা ঠিক?

চেহারায় মিল আছে, কাজে নেই। দুই জায়গাতেই action যায়, reducer নতুন state দেয়। React-এর useReducer-এ এই ধারণা এসেছে Redux আর Flux-এর ধারা থেকে। তাই ২০১৯ সালে hook আসার পর অনেকে লিখলেন, “Redux আর লাগবে না।”

পার্থক্য দেখা যায় render-এ। Redux-এর store থাকে React-এর বাইরে। Component একটি selector দিয়ে সাবস্ক্রাইব করে, আর selector-এর ফল না বদলালে render হয় না। Context-এ provider-এর মান বদলালে প্রতিটি consumer render হয়। ২০২১ সালে Redux-এর রক্ষণাবেক্ষণকারী Mark Erikson তাঁর “Why React Context is Not a State Management Tool” লেখায় ঠিক এই কথাটি বলেন: useReducer নতুন state দিলে context-এর সব subscriber render হয়, তারা যে অংশই চাক। তিনি context-কে বলেন পরিবহন ব্যবস্থা, ব্যবস্থাপনা নয়।

Redux-এ আরও আছে middleware, action-এর ইতিহাস দেখার devtools, আর React-এর বাইরে থেকে store ব্যবহারের সুযোগ। অন্যদিকে context-এর সুবিধাও সত্যি: কোনো dependency নেই, আর subtree ধরে আলাদা মান দেওয়া যায়, যা একটি global store-এ সহজ নয়। একই পাতায় দুটি আলাদা cart-এর ফর্ম থাকলে দুটি provider-ই যথেষ্ট।

তাই প্রশ্নটি “কে ভালো” নয়। প্রশ্ন হলো, আপনার consumer কয়টি, মান কত ঘন ঘন বদলায়, আর ইতিহাস বা middleware লাগে কি না। ছোট cart-এ দুটিই চলবে; হাজারো সারির dashboard-এ context-এ কষ্ট হবে।

মূল উপলব্ধি: একই নকশা মানেই একই খরচ নয়। State কোথায় থাকে আর কে কীভাবে সাবস্ক্রাইব করে, সেটিই পার্থক্য গড়ে।

প্র ০২React-এ useContextSelector নেই কেন? সবাই তো চায়!

চাহিদা সত্যিই অনেক। ধারণাটি সহজ: useContextSelector(Cart, s => s.count) লিখলে শুধু count বদলালেই render হবে। React-এর RFC আলোচনায় এমন প্রস্তাব এসেছে, আর use-context-selector নামে একটি community library-ও আছে। তবু লেখার সময়ে React 19.3.1-এর export তালিকায় এমন কিছু নেই; আমরা নিজে তালিকা দেখে এটি নিশ্চিত করেছি।

কারণ বুঝতে হলে খরচটি দেখুন। Selector থাকলে প্রতিটি মান বদলের সময় প্রতিটি consumer-এর selector চালাতে হয়, আর তার আগের ফলের সাথে মেলাতে হয়। Context-এর বর্তমান পথে React ট্রি ধরে consumer খোঁজে; এর সাথে নিরাপদে selector জোড়া দেওয়া আর concurrent rendering-এর সাথে মিলিয়ে চলা সহজ কাজ নয়। এখানে আমরা React দলের নির্দিষ্ট কোনো সিদ্ধান্তের কারণ দাবি করছি না; শুধু সাধারণ প্রকৌশল-কারণগুলো বলছি।

বাস্তবে এখন তিনটি পথ আছে। এক, context ভাগ করা, যা অংশ ৪-এ করলেন। দুই, মাঝে একটি memo করা component রেখে শুধু দরকারি অংশ prop হিসেবে দেওয়া: ওপরের wrapper context পড়ে, নিচেরটি ভারী কাজ করে। Wrapper render হলেও ভারী অংশ বেঁচে যায়। তিন, external store, যেখানে selector আগে থেকেই আছে।

এখানে React Compiler-এর ভূমিকাও ভাবুন। Compiler JSX-এর অংশ memoise করতে পারে, ফলে consumer render হলেও ভারী subtree-র কাজ কমে। এতে render গণনা না কমলেও আসল খরচ কমতে পারে। তাই render-সংখ্যা আর সময়, দুটি আলাদা জিনিস মাপুন।

মূল উপলব্ধি: একটি ফিচার “কেন নেই” বোঝার চেষ্টা করলে সিস্টেমের আসল সীমা শেখা যায়। আর যা নেই, তার জন্য বিকল্প নকশা প্রায়ই আগে থেকেই থাকে।

প্র ০৩Reducer pure হওয়ার শর্তটি কেন এত কড়া? দুবার ডাকলে কী এমন ক্ষতি?

এই শর্ত থেকেই reducer-এর সব ক্ষমতা আসে। React 18-এর পর থেকে concurrent rendering আছে। এতে React একটি render শুরু করে মাঝপথে ফেলে দিতে পারে, পরে আবার চালাতে পারে। Reducer যেহেতু render-এর ভেতরে চলে, তাই সেও এক action-এর জন্য একাধিকবার চলতে পারে। ফল ফেলে দেওয়া হলে কোনো ক্ষতি হয় না, শুধু যদি reducer বাইরের কিছু না ছোঁয়।

ধরুন reducer-এ fetch('/api/orders', { method: 'POST' }) লেখা আছে। development-এ StrictMode দুবার ডাকল, তাই দুটি অর্ডার গেল। production-এ হয়তো বেশিরভাগ সময় একটি, কিন্তু render বাতিল হলে আরও বিপদ। এটি এমন বাগ, যা শুধু মাঝে মাঝে দেখা যায়। টাকার সঠিক হিসাবে এমন ভুল চলে না। এজন্যই StrictMode ইচ্ছা করেই দুবার ডাকে: ভুল আগেভাগে চোখে পড়ে।

আরেকটি লাভ হলো ইতিহাস। Pure reducer আর action-এর তালিকা থাকলে শুরুর state থেকে সব আবার চালিয়ে যেকোনো মুহূর্তের state পাওয়া যায়। ২০১৫ সালের একটি বিখ্যাত বক্তৃতায় Dan Abramov এভাবেই time-travel debugging দেখান। হিসাব খাতার মতো: প্রতিটি লেনদেন লেখা থাকলে যেকোনো দিনের ব্যালান্স বের করা যায়। ডেটাবেসের log আর event sourcing-এর পেছনেও একই চিন্তা।

তাহলে side effect কোথায় যাবে? Click-এর ফল হলে event handler-এ। State বদলের সাথে মেলানোর কাজ হলে effect-এ। ID বা সময় লাগলে handler-এ তৈরি করে action-এ পাঠান: dispatch({ type: 'added', id: crypto.randomUUID() }).

মূল উপলব্ধি: বিধিনিষেধ অনেক সময় নিজেই ক্ষমতা। Reducer কিছুই করতে পারে না বলেই তাকে test করা, আবার চালানো আর বিশ্বাস করা যায়।

প্র ০৪পুরোনো context API বাদ দিয়ে ২০১৮ সালে নতুন API আনতে হলো কেন?

React 16.3-এর আগে একটি পরীক্ষামূলক API ছিল: parent-এ childContextTypes আর getChildContext, child-এ contextTypes. Docs নিজেই সাবধান করত, এটি বদলাতে পারে, ব্যবহার এড়িয়ে চলুন। তবু React Router বা Redux-এর binding-এর মতো library এটি ভেতরে ব্যবহার করত, কারণ prop drilling ছাড়া দূরে মান পাঠানোর অন্য পথ ছিল না।

সবচেয়ে বড় সমস্যা ছিল আপডেট আটকে যাওয়া। মাঝের কোনো component shouldComponentUpdate থেকে false দিলে নিচের child আর নতুন মান পেত না। অর্থাৎ optimization করতে গিয়ে তথ্যের পথ কেটে যেত। মান পৌঁছানো নির্ভর করত মাঝের component render হওয়ার ওপর। আপনি হয়তো ভুলটি ধরতে পারতেন না, কারণ পুরোনো মান দিব্যি দেখা যেত।

নতুন API, যা ২০১৮ সালে React 16.3-তে এলো, এই পথই বদলে দিল। Consumer সরাসরি provider-এর সাথে যুক্ত। মান বদলালে React নিজে consumer খুঁজে render করে। মাঝের memo বা shouldComponentUpdate আর বাধা নয়। অংশ ৩-এর “memo আটকায় না” আসলে কোনো দোষ নয়; এটিই ছিল সেই নকশার মূল উদ্দেশ্য। আগে যা ছিল বাগ, এখন তা নিয়ম।

তবে এই সমাধানের দাম আছে, আর সেটিই আজকের পর্বের মূল সমস্যা: consumer-কে আটকানোর কোনো সহজ পথ নেই। নতুন API পাশাপাশি আরও সহজ করেছে ব্যবহার। ২০১৯ সালে useContext Consumer-এর render prop-কে অনেকটা সরিয়ে দিল। React 19-এ .Provider লেখাও আর লাগে না।

মূল উপলব্ধি: ভালো API নকশা ভুলকে অসম্ভব করে দেয়। একটি সমস্যা সমাধান করলে নতুন একটি trade-off জন্ম নেয়, আর সেটি বোঝাই প্রকৌশল।

প্র ০৫Context কি আসলে dependency injection? এটি কোথায় সত্যিই শক্তিশালী?

হ্যাঁ, অনেকটাই। পর্ব ০৪-এ দেখেছেন, dependency injection মানে যে কাজ করবে সে নিজে নির্ভরতা তৈরি করে না, বাইরে থেকে পায়। Component useContext(ApiContext) লিখলে সে জানে না api কোথা থেকে এলো। ওপরের provider ঠিক করে দেয়।

এই শক্তি সবচেয়ে ভালো দেখা যায় test-এ আর subtree-তে। Test-এ <ApiContext value={fakeApi}> দিয়ে ঘিরলেই আসল সার্ভার ছাড়া component চলে। “বাজারঘর”-এর admin অংশের একটি ছোট preview-তে আলাদা ব্যবহারকারী বা ভাষা দেখাতে চাইলে শুধু সেই অংশে আরেকটি provider বসান। React docs-ও একই কথা বলে: nested provider দিয়ে ট্রির একটি অংশে মান বদলানো যায়। Global store-এ এটি অনেক ঝামেলার।

অনেক library ভেতরে এভাবেই context ব্যবহার করে। Docs নিজেই বলে, বেশিরভাগ routing সমাধান context-এ বর্তমান route রাখে। Form library, theme library, এমনকি component library-র compound component, যেমন পর্ব ০৯-এর Tabs, এই পথে চলে। এখানে মান সাধারণত একটি subtree-তে সীমিত, consumer কম, তাই re-render-এর ভয়ও কম।

তবে সতর্ক থাকুন। Context-এ নির্ভরতা লুকানো থাকে। Props দেখলে component কী চায় বোঝা যায়, কিন্তু useContext ফাইলের ভেতরে লুকিয়ে থাকে। একটি component আলাদা করে তুলে নিতে গেলে দেখবেন সে পাঁচটি provider ছাড়া চলে না। নতুন সহকর্মীর জন্য এটি একটি বাধা। Provider-এর সারি খুব লম্বা হলে সেটিও একটি সংকেত।

মূল উপলব্ধি: Context-এর সেরা ব্যবহার সেখানে, যেখানে মান কম বদলায় আর “কে দিচ্ছে” পাল্টানোর স্বাধীনতা দরকার। এটি লুকানো নির্ভরতার দামে পাওয়া সুবিধা।

অনুশীলন

আগে নিজে করুন, তারপর উত্তর মেলান।

  1. হিসাব: একটি পাতায় ৩০টি ProductCard, ১টি Header আর ২টি MiniCart আছে। সবাই একই context পড়ে। কার্ড শুধু add ব্যবহার করে। ১৫ বার click হলো। মোট কতটি consumer render? কার্ডগুলো আলাদা action context-এ নিলে কত? কত শতাংশ কমল?

    এক context-এ consumer = ৩০ + ১ + ২ = ৩৩, তাই ৩৩ × ১৫ = ৪৯৫ render. ভাগ করলে শুধু Header আর MiniCart: ৩ × ১৫ = ৪৫. কমেছে (৪৯৫ − ৪৫) ÷ ৪৯৫ = ৪৫০ ÷ ৪৯৫ = ৯০.৯%. ধরে নেওয়া হয়েছে প্রতি click-এ count সত্যিই বদলায়, আর action-এর মান স্থির।

  2. ফল আগে বলুন: Cart খালি। ডালের দাম ১৪০, তেলের দাম ১৯০। অংশ ৭-এর reducer-এ পরপর চলল: added(dal), added(dal), added(tel), qty_set(id: tel, qty: 0), qty_set(id: dal, qty: 5). শেষে state কী, মোট কত? আর এই চারটি dispatch একই handler-এ হলে, handler-এর শেষ লাইনের console.log(state.items.length) কী ছাপবে?

    ধাপে ধাপে: ডাল qty ১ → ডাল qty ২ → ডাল ২ + তেল ১ → তেল মুছে ডাল ২ → ডাল ৫। শেষ state: { items: [{ id: dal, qty: 5, … }] }. মোট ৫ × ১৪০ = ৭০০. console.log ছাপবে 0, কারণ handler চলাকালীন state সেই render-এর snapshot, যেখানে cart খালি ছিল। পরের render-এ ১ দেখাবে। সব action একটিই render-এ সারি ধরে চলবে।

  3. নকশা সমালোচনা: একজন লিখেছেন AppContext, যার value { user, theme, lang, cart, products, searchText, isModalOpen }. সব কিছু একটি useState-এ। সার্চ বক্সে টাইপ করলেই পাতা আটকে আসে। কী কী সমস্যা, আর কীভাবে ভাগ করবেন?

    প্রতিটি অক্ষরে নতুন value, তাই সব consumer render হচ্ছে। ভাগ করুন বদলের হার দেখে। searchText আর isModalOpen global নয়: যে component-এ লাগে, সেখানেই local state; দরকারে URL-এ। products সার্ভারের ডেটা: server state টুলে। user, theme, lang কম বদলায়: প্রতিটি আলাদা context. cart: reducer + state ও dispatch-এর আলাদা context, অথবা ঘন ব্যবহার হলে store. তাড়াহুড়ো নয়: আগে Profiler-এ মাপুন, তারপর একটি একটি করে সরান।

  4. Command-এর কাজ: অংশ ৭-এর CartAction-এ { type: "coupon_applied"; percent: number } যোগ করুন, reducer-এ কিছু না লিখে। npx tsc --noEmit কী বলবে? তারপর CartState-এ discount যোগ করে case লিখুন, আর একটি Vitest test দিন: ৪৭০ টাকায় ১০% ছাড়ে মোট ৪২৩।

    tsc assertNever(action) লাইনে TS2345 দেবে: { type: "coupon_applied"; percent: number; } never-এ বসানো যায় না। এটিই exhaustive check-এর কাজ। Case: return { ...state, discount: action.percent };. cartTotal-এ যোগফল নিয়ে × (100 − discount) ÷ 100. ৪৭০ × ৯০ ÷ ১০০ = ৪২৩। Test: expect(cartTotal({ ...s, discount: 10 })).toBe(423). ভালো অভ্যাসে percent-এর সীমাও যাচাই করুন, আর আসল অর্ডারের দাম ও ছাড় সবসময় সার্ভারে আবার হিসাব করুন; ব্রাউজারের মান বিশ্বাস করা যায় না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমuseReducer-এ এক click-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🖱️ ব্যবহারকারী বাটনে click করলেন, handler চলল
  2. 📨 dispatch(action) action-টি সারিতে জমা রাখল
  3. ⚙️ React component আবার ডাকল, render শুরু
  4. 🧮 useReducer-এর ভেতরে reducer চলে নতুন state দিল
  5. 🔍 React নতুন JSX আগের সাথে diff করল
  6. 🎨 Commit হলো, ব্রাউজার স্ক্রিন আঁকল

React-এ createContext('light')-এর 'light' মান কখন পাওয়া যায়?

  • যেকোনো useContext ডাকলে সবসময়
  • যখন component-এর ওপরে ওই context-এর কোনো provider নেই
  • যখন provider আছে কিন্তু value দেওয়া হয়নি
  • যখন provider-এর value হয় undefined

একটি memo করা component useContext(CartContext) পড়ে। Props একই, কিন্তু context-এর মান বদলাল। কী ঘটবে?

  • Render হবে না, memo আটকে দেবে
  • Parent render হলে তবেই render হবে
  • আবার render হবে, কারণ memo context-এর পরিবর্তন আটকায় না
  • React একটি error দেবে

Provider-এ value={{ user }} লেখা আছে, user বদলায়নি, কিন্তু parent আবার render হলো। Consumer-দের কী হবে?

  • সবাই আবার render হবে, কারণ object-টি নতুন
  • কেউ render হবে না, কারণ user একই
  • শুধু প্রথম consumer render হবে
  • React error দেবে

কোনো handler-এ console.log(state.n) ছাপল 5. এরপর dispatch({ type: 'inc' }), তারপর আবার console.log(state.n). দ্বিতীয় লাইনে কী ছাপবে?

  • 6
  • 5
  • undefined
  • Error

React reducer-এ কোনটি রাখা ঠিক?

  • localStorage.setItem('cart', ...)
  • fetch('/api/cart', ...)
  • Math.random() দিয়ে id তৈরি
  • Spread দিয়ে নতুন object ফেরত দেওয়া: { ...state, n: state.n + 1 }

TypeScript reducer-এর switch-এর default-এ assertNever(action) রাখার মূল লাভ কী, যেখানে assertNever-এর প্যারামিটারের type never?

  • Reducer দ্রুত চলে
  • Unit test আর লিখতে হয় না
  • কোনো action-এর case বাদ পড়লে compile-এর সময় error হয়
  • Consumer-এর re-render কমে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Context ধরার আগে props আর composition; Context শুধু মান পৌঁছায়, state রাখে না।
  2. React 19-এ <C value>; পড়া useContext বা use. Default শুধু provider না থাকলে।
  3. মান বদলালে প্রতিটি consumer render, memo থাকলেও। ভাগ করুন, memoise করুন, মাপুন।
  4. Reducer pure, render-এ চলে; dispatch চলতি state বদলায় না।
  5. Cart: state ও dispatch আলাদা context, useCart hook, typed action, React ছাড়া test; ঘন বদল আর selector লাগলে store.

পরের পর্বে: Context-এর স্থায়ী সীমাটি দেখলেন: সবাই একসাথে render হয়। এবার store নিয়ে আসি, যেখানে selector দিয়ে শুধু দরকারি অংশ পড়া যায়: পর্ব ১৮ · Zustand আর Redux Toolkit.

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান