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

State কোথায় রাখবেন — local, lifted, global আর server state

Where State Lives — Local, Lifted, Global and Server State

Taught in Bangla, this lesson sorts state into five kinds (UI, form, URL, client global and server cache), keeps state close to where it is used, measures the cost of state held too high, avoids storing derived values, puts filters and tabs in the URL, treats server state as a cache with a decision chart, compares props, Context, Zustand and Redux, uses state machines to remove impossible states, and weighs localStorage risks in the Bazarghar state map.

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

  • পাঁচ রকম state চেনা, আর একটি প্রশ্নমালায় প্রতিটির জায়গা ঠিক করা
  • State ওপরে রাখার re-render খরচ হিসাব করা আর মাপা
  • URL-কে state হিসেবে ব্যবহার করা, যাতে লিংক শেয়ার করা যায়
  • Prop drilling, Context, Zustand আর Redux-এর মধ্যে বেছে নেওয়ার যুক্তি
  • State machine দিয়ে অসম্ভব অবস্থা বাদ দেওয়া, আর localStorage-এর ঝুঁকি বোঝা
  • “বাজারঘর”-এর পুরো state মানচিত্র আঁকা

১ · সব state এক জাতের নয়

একটি মুদি দোকান ভাবুন। ক্রেতা এইমাত্র কী চাইলেন, তা দোকানির মনে আছে। সেটি একটু পরেই মুছে যাবে। বাকির হিসাব আছে খাতায়, দোকান বন্ধ হলেও থাকবে। আসল মাল আছে গুদামে, আর তাকে সাজানো মাল গুদামের একটি কপি মাত্র। দোকানের সামনের সাইনবোর্ড দেখে যে কেউ বলতে পারে, এটি কোন দোকান।

তথ্যের জাত আলাদা, তাই রাখার জায়গাও আলাদা। React অ্যাপেও ঠিক তাই। পর্ব ১০-এ আপনি useState দিয়ে state শিখেছেন, আর পর্ব ১২-এ form. কিন্তু অ্যাপ বড় হলে আসল প্রশ্ন আর “কীভাবে” থাকে না, হয়ে যায় “কোথায়”। আগে জাতগুলো চিনে নিন।

জাতমানেবাজারঘরে উদাহরণকতক্ষণ বাঁচে
UI stateপর্দার ছোটখাটো অবস্থাmodal খোলা কি না, ড্রপডাউনcomponent থাকা পর্যন্ত
Form stateলেখা চলছে, এখনো জমা হয়নিকুপন, ঠিকানার ঘরsubmit পর্যন্ত
URL stateaddress bar-এ থাকা অবস্থাcategory, sort, pageলিংক যতদিন থাকে
Client globalশুধু ব্রাউজারের, বহু জায়গায় লাগেguest কার্ট, themetab খোলা থাকা পর্যন্ত, চাইলে তার পরেও
Server cacheআসল কপি server-এ, এখানে শুধু ছায়াপণ্যের তালিকা, দাম, stockযতক্ষণ নতুন বলে ধরা হয়
মূল ধারণা

“State management” কোনো library বাছাইয়ের প্রশ্ন নয়। প্রতিটি তথ্যের জন্য তিনটি প্রশ্ন: মালিক কে, কে কে পড়ে, কতক্ষণ বাঁচতে হবে। এই তিনটির উত্তরই জায়গা ঠিক করে দেয়।

২ · Colocation — যার দরকার তার কাছে রাখুন

রান্নাঘরের লবণ রান্নাঘরেই থাকে, বসার ঘরের আলমারিতে নয়। State-এরও একই নিয়ম। Colocation মানে একটি state-কে সেই component-এর যত কাছে সম্ভব রাখা, যে সেটি ব্যবহার করে। এতে তিনটি লাভ: কোড পড়া সহজ, component মুছলে state-ও মুছে যায়, আর অন্য component অকারণে render হয় না।

কিন্তু দুটি component-এর একই তথ্য লাগলে? তখন পর্ব ১০-এর lifting state up: state তুলে নিন দুজনের সবচেয়ে কাছের সাধারণ parent-এ। এটি ঠিক পথ, তবে তিনটি জায়গায় ব্যথা শুরু হয়।

Prop drilling

State চার স্তর ওপরে উঠলে মাঝের component-গুলো অকারণে props বয়ে নেয়। পর্ব ৯-এর composition অনেক সময় এটি কমায়।

বেশি re-render

Parent-এর state বদলালে তার নিচের সব child আবার render হয়, তারা মানটি না পড়লেও।

দায় ঝাপসা

ওপরের component-এ ডজনখানেক state জমলে সেটি god component হয়ে যায়। কোনটি কার, বোঝা কঠিন।

এক নিয়মে মনে রাখুন: তুলুন ঠিক ততটুকু, যতটুকু দরকার। আর কোনো state আর ভাগ করার দরকার না থাকলে, আবার নামিয়ে দিন। নামানোও একটি স্বাভাবিক refactor. নিচের live ব্লকে একই কুপন ঘর দুই জায়গায় রাখা হয়েছে। দুই বাক্সে EID লিখে নিচের আউটপুট দেখুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const names = ['চাল', 'ডাল', 'তেল'];
const count = { high: 0, low: 0 };
function Card({ name, side }) {
  count[side]++;                         // প্রতি render-এ এক বাড়ে
  return <li>{name}</li>;
}
function Coupon({ code, setCode }) {
  return <input placeholder="কুপন" value={code} onChange={e => setCode(e.target.value)} />;
}
function HighShop() {                    // state ওপরের দোকান component-এ
  const [code, setCode] = useState('');
  useEffect(() => console.log('high code=' + code, 'Card renders:', count.high));
  return <section><b>state ওপরে</b> <Coupon code={code} setCode={setCode} />
    <ul>{names.map(n => <Card key={n} name={n} side="high" />)}</ul></section>;
}
function LocalCoupon() {                 // state শুধু যার দরকার তার কাছে
  const [code, setCode] = useState('');
  useEffect(() => console.log('low  code=' + code, 'Card renders:', count.low));
  return <Coupon code={code} setCode={setCode} />;
}
function LowShop() {
  return <section><b>state নিচে</b> <LocalCoupon />
    <ul>{names.map(n => <Card key={n} name={n} side="low" />)}</ul></section>;
}
createRoot(document.getElementById('root')).render(<><HighShop /><LowShop /></>);
</script>

Edge-এ চালিয়ে দুই বাক্সে EID লিখলে যা এলো:

high code= Card renders: 3
low  code= Card renders: 3
high code=E Card renders: 6
high code=EI Card renders: 9
high code=EID Card renders: 12
low  code=E Card renders: 3
low  code=EI Card renders: 3
low  code=EID Card renders: 3

শুরুতে দুই পাশেই ৩টি Card, ৩ বার render. এরপর ওপরের পাশে প্রতি অক্ষরে দোকানটি আবার render হলো, তাই তিনটি Card-ও, ফলে 3 + 3×3 = 12. নিচের পাশে শুধু LocalCoupon render হলো। Card-এর সংখ্যা 3-তেই আটকে আছে। কোড প্রায় একই, পার্থক্য শুধু state-এর ঠিকানা। এটি production React, তাই StrictMode-এর দ্বিগুণ render এখানে নেই।

৩ · ওপরের state-এর দাম — হিসাব করে দেখুন

এবার গণনাটি ধাপে ধাপে দেখুন। গাছটি এমন: App-এর নিচে Header, Coupon আর ProductList, আর তার নিচে ৩টি Card. প্রথমে state আছে App-এ, পরে সেটি নামবে Coupon-এ। “পরে” চেপে সংখ্যাগুলো দেখুন।

App.jsxstate ওপরে
function App() {
  const [code, setCode] = useState('');
  return <>
    <Header />
    <Coupon code={code} setCode={setCode} />
    <ProductList />
  </>;
}
App.jsxstate নিচে
function App() {
  return <>
    <Header />
    <Coupon />
    <ProductList />
  </>;
}
function Coupon() {
  const [code, setCode] = useState('');
  return <input value={code} onChange={...} />;
}
এই অক্ষরে কে কতবার render হলো
App
Header
Coupon
ProductList
Card ×3
এই অক্ষরে মোট
state ওপরে, জমা মোট
state নিচে, জমা মোট
  1. শুরু। পাতা খুলেছে, প্রথম render শেষ। এখন থেকে শুধু টাইপের render গুনব।
  2. প্রথম অক্ষর E. setCode ডাকা হলো App-এ। তাই App আর তার নিচের সবাই render হলো: 1 + 1 + 1 + 1 + 3 = 7.
  3. দ্বিতীয় অক্ষর I. আবার ৭টি। জমা হলো 14. অথচ Header বা Card-এর কিছুই বদলায়নি।
  4. State নামানো হলো। code কি শুধু Coupon পড়ে? হ্যাঁ। তাই state কেটে Coupon-এ বসালাম। App-এ আর কোনো state নেই। এবার নতুন করে গুনব।
  5. আবার E. এবার setCode চলছে Coupon-এ। React সেখান থেকে নিচে দেখে, ওপরে নয়। মোট 1.
  6. আবার I. আবার ১টি। জমা 2. কোনো memo ছাড়াই এই সাশ্রয় এলো।
  7. পুরো কুপন, ৬ অক্ষর। State ওপরে: 6 × 7 = 42. State নিচে: 6 × 1 = 6. কার্ডের সংখ্যা যত বাড়বে, এই ফারাক তত বাড়বে।

হিসাবটি সূত্রে

ধরুন state যে component-এ আছে, তার নিচে মোট Sটি component. তাহলে এক অক্ষরে render হয় S + 1টি, আর k অক্ষরে k × (S + 1)টি। প্রতিটি গড়ে t মিলিসেকেন্ড নিলে এক অক্ষরের খরচ (S + 1) × t. Frame budget মনে রাখুন: 60 fps-এ এক frame পায় 1000 ÷ 60 ≈ 16.7 ms, আর তার ভেতরেই style, layout আর paint-ও হতে হয়।

গণনার উদাহরণ: ধরা যাক ২০০টি Card, আর ওপরে ৩টি component (App, Header, Coupon) ও ProductList. মোট ২০৪টি component. প্রতিটিতে অনুমিত t = 0.05 ms হলে এক অক্ষরে 204 × 0.05 = 10.2 ms. Frame budget-এর 16.7 ms থেকে বাকি থাকে 6.5 ms. ৭ অক্ষরের “basmati” লিখলে render হয় 7 × 204 = 1428টি। State নিচে নামালে ৭টি। t-এর মান এখানে শুধু একটি ধরে নেওয়া সংখ্যা, মাপা ফল নয়।

এবার মাপা ফল। ২০০০টি সরল Card-এর তালিকায় এক অক্ষরের commit-এর সময় মাপা হলো। সত্যিকারের run: production React 19, Edge, Playwright, প্রতিটি ক্ষেত্রে ৫০ বার। CPU-কে ৪ গুণ ধীর করে কম দামি ফোনের কাছাকাছি একটি অবস্থাও দেখা হলো।

cpu x1 high median 1.55 ms  max 3.00 ms
cpu x1 low  median 0.00 ms  max 0.10 ms
cpu x4 high median 6.50 ms  max 12.30 ms
cpu x4 low  median 0.00 ms  max 0.30 ms

এটি একটি যন্ত্রে একবারের মাপ, সাধারণ ফল নয়। এখানে timer-এর সূক্ষ্মতা প্রায় 0.1 ms, তাই 0.00 মানে “মাপার সীমার নিচে”, শূন্য নয়। দুটি শিক্ষা নিন। এক: ২০০০টি সরল Card-ও এই যন্ত্রে budget-এর ভেতরেই ছিল। তাই শুধু re-render গুনে ভয় পাবেন না। দুই: ধীর CPU-তে সবচেয়ে খারাপ মান 12.3 ms-এ উঠেছে, অথচ Card-গুলো ছিল খুব হালকা। ছবি, দাম হিসাব আর chart থাকলে খরচ আরও বাড়তে পারে। State নামানোর আসল কারণ তাই আগে পরিষ্কার কোড, তারপর গতি।

আরও গভীরে: React পাশের বা ওপরের component render করে না, শুধু state-এর মালিক আর তার নিচের অংশ দেখে। Child-কে children হিসেবে পাঠালে তার element আগের render-এই তৈরি, তাই মান একই থাকলে React সেটি বাদ দিতে পারে। React Compiler অনেক অকারণ render কমায়, তবে state-এর মালিক কে, সেই প্রশ্নের উত্তর দেয় না। মাপার পুরো পদ্ধতি আসবে পর্ব ২৩-এ। পর্ব ১৫-এ দেখেছেন render আর commit আলাদা, তাই render হলেই DOM বদলায় না।

৪ · Derived state — এক তথ্য, এক ঠিকানা

চায়ের দোকানের খাতায় প্রতি লাইনে চায়ের সংখ্যা লেখা আছে। মোট বিল আলাদা করে লিখে রাখলে কী হবে? একটি লাইন কাটা গেলে মোট বদলাতে ভুল হতে পারে, আর তখন খাতার দুই জায়গা দুই কথা বলবে। মোটটি দরকারের সময় যোগ করে নিলেই এই ঝামেলা থাকে না।

এটাই derived state-এর মূল কথা। পর্ব ১০-এ পাঁচ নিয়ম দেখেছেন, আর পর্ব ১৩-এ দেখেছেন এর জন্য Effect লাগে না। এখানে কথাটি নকশার। প্রতিটি তথ্যের একটি single source of truth থাকবে, বাকি সব সেখান থেকে হিসাব হবে। নিচের কার্টে “চাল আরেকটি”, “চালের দাম ৳70”, আবার “চাল আরেকটি” চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const start = [{ id: 'rice', name: 'চাল', qty: 2, price: 80 }, { id: 'dal', name: 'ডাল', qty: 1, price: 140 }];
function Cart() {
  const [items, setItems] = useState(start);
  const [savedTotal, setSavedTotal] = useState(300);          // কপি: ভুলের উৎস
  const total = items.reduce((sum, i) => sum + i.qty * i.price, 0); // derived
  useEffect(() => console.log('copy:', savedTotal, ' derived:', total));
  function addRice() {
    const rice = items.find(i => i.id === 'rice');
    setItems(items.map(i => i === rice ? { ...i, qty: i.qty + 1 } : i));
    setSavedTotal(savedTotal + rice.price);                   // মনে করে বদলালাম
  }
  function priceDrop() {
    setItems(items.map(i => i.id === 'rice' ? { ...i, price: 70 } : i));
    // এখানে setSavedTotal ডাকতে ভুলে গেলাম
  }
  return <div>
    <ul>{items.map(i => <li key={i.id}>{i.name}: {i.qty} × ৳{i.price}</li>)}</ul>
    <button onClick={addRice}>চাল আরেকটি</button>
    <button onClick={priceDrop}>চালের দাম ৳70</button>
    <p>কপি করা মোট: ৳{savedTotal}</p>
    <p><b>Derived মোট: ৳{total}</b></p>
  </div>;
}
createRoot(document.getElementById('root')).render(<Cart />);
</script>

Edge-এ চালিয়ে ওই তিনটি বোতাম চাপলে যা এলো:

copy: 300  derived: 300
copy: 380  derived: 380
copy: 380  derived: 350
copy: 450  derived: 420

প্রথম চাপে দুটোই 380. দ্বিতীয় চাপে দাম কমল, total নিজে থেকেই 350 হলো (3 × 70 + 140), কিন্তু কপিটি 380-তেই রইল। তৃতীয় চাপে কপি সঠিকভাবে ৭০ বাড়ল, তবু 450 বনাম 420, মানে ৩০ টাকার ভুল আর মেটে না। লক্ষ করুন, কোনো error আসেনি। ক্রেতা শুধু ভুল বিল দেখছেন।

Duplicate-এর আরও দুটি পরিচিত চেহারা আছে। এক: prop-কে state-এ কপি করা, যেমন useState(price). Parent দাম বদলালেও এই state বদলায় না, কারণ initial মান শুধু প্রথম render-এ পড়া হয়। দুই: তালিকার পুরো object কপি করা। selectedProduct না রেখে শুধু selectedId রাখুন, আর render-এ find করুন। হিসাব সত্যিই ভারী হলে পর্ব ১৪-এর useMemo আছে, কিন্তু সেটিও জমা রাখা নয়, হিসাবের cache মাত্র।

৫ · URL-ও state — লিংকেই পুরো অবস্থা

রিকশাওয়ালাকে পুরো ঠিকানা বললে যে কেউ একই জায়গায় পৌঁছায়। URL ঠিক তেমন একটি ঠিকানা। ধরুন বাজারঘরে একজন “তেল, দামে কম থেকে বেশি, পাতা ৩” বেছে বন্ধুকে লিংক পাঠালেন। Filter যদি শুধু useState-এ থাকে, বন্ধু দেখবেন প্রথম পাতার সাধারণ তালিকা। Refresh দিলে আপনিও তাই দেখবেন। আর filter যদি query string-এ থাকে, লিংকটিই পুরো অবস্থা বহন করে।

/products?cat=oil&sort=price&page=3

এর সাথে বিনা খরচে পাওয়া যায় চারটি সুবিধা: refresh-এ অবস্থা থাকে, Back বোতাম কাজ করে, deep link শেয়ার করা যায়, আর bookmark করা যায়। নিচের ব্লকে address bar-এর একটি নকল আছে, কারণ sandbox-এ আসল URL বদলানো যায় না। লক্ষ করুন, cat আর page-এর জন্য আলাদা useState নেই। সবই URL থেকে হিসাব হচ্ছে। “পরের পাতা”, “তেল”, “লিংক পাঠান”, আবার “পরের পাতা” আর “Back” চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const all = [['মিনিকেট', 'rice'], ['নাজিরশাইল', 'rice'], ['চিনিগুঁড়া', 'rice'], ['সয়াবিন', 'oil'], ['সরিষা', 'oil']];
function Shop({ start, label, onShare }) {
  const [stack, setStack] = useState([start]);           // address bar-এর নকল
  const url = stack.at(-1), query = new URLSearchParams(url.split('?')[1]); // URL থেকেই পড়া
  const cat = query.get('cat') ?? 'rice';
  const page = Number(query.get('page') ?? 1);
  const rows = all.filter(p => p[1] === cat).slice((page - 1) * 2, page * 2).map(p => p[0]);
  useEffect(() => console.log(label, url, '=>', rows.join(', ') || '(empty)'), [url]);
  function go(change) {
    const next = new URLSearchParams(query);
    for (const k in change) next.set(k, change[k]);
    setStack([...stack, '/products?' + next]);
  }
  return <div><p><b>{label}:</b> <code>{url}</code></p>
    <button onClick={() => go({ cat: 'oil', page: 1 })}>তেল</button>
    <button onClick={() => go({ page: page + 1 })}>পরের পাতা</button>
    <button disabled={stack.length < 2} onClick={() => setStack(stack.slice(0, -1))}>Back</button>
    {onShare && <button onClick={() => onShare(url)}>লিংক পাঠান</button>}
    <p>{rows.join(', ') || 'কিছু নেই'}</p></div>;
}
function App() {
  const [link, setLink] = useState(null);
  return <><Shop start="/products?cat=rice&page=1" label="me" onShare={setLink} />
    {link && <Shop key={link} start={link} label="friend" />}</>;
}
createRoot(document.getElementById('root')).render(<App />);
</script>

Edge-এ চালিয়ে ওপরের “me” অংশে সেই ক্রমে চাপলে যা এলো:

me /products?cat=rice&page=1 => মিনিকেট, নাজিরশাইল
me /products?cat=rice&page=2 => চিনিগুঁড়া
me /products?cat=oil&page=1 => সয়াবিন, সরিষা
friend /products?cat=oil&page=1 => সয়াবিন, সরিষা
me /products?cat=oil&page=2 => (empty)
me /products?cat=oil&page=1 => সয়াবিন, সরিষা

friend-এর লাইনটি দেখুন। সে শুধু একটি লিংক পেয়েছে, তবু একই তেলের তালিকা দেখছে। page=2-তে তালিকা খালি, আর Back এক ধাপ ফিরিয়ে এনেছে। আরও একটি জিনিস: “তেল” চাপলে page নিজে থেকেই 1 হয়েছে। এটি ভুলে গেলে ক্রেতা পাতা ৩-এ বসে থাকেন, যেখানে নতুন filter-এ কিছুই নেই।

আসল অ্যাপে address bar পড়া ও বদলানোর কাজ করে router. React Router-এ useSearchParams আছে, লেখার সময়ে version 8.5.1. পুরোটা শিখবেন পর্ব ২০-এ, এখানে শুধু আকারটি দেখুন। এটি উদাহরণ, এখানে চালানো হয়নি।

import { useSearchParams } from 'react-router';

function ProductsPage() {
  const [params, setParams] = useSearchParams();
  const cat = params.get('cat') ?? 'all';
  const pick = (next) => setParams((old) => {
    old.set('cat', next);
    old.set('page', '1');   // filter বদলালে প্রথম পাতায়
    return old;
  });
  // ...
}

URL ব্যবহারের চার সতর্কতা। (১) URL হলো ব্যবহারকারীর ইনপুট। ?page=-5 বা ?page=abc আসতে পারে, তাই পড়ার সময় যাচাই করুন। (২) Token, password বা ব্যক্তিগত তথ্য URL-এ নয়, কারণ URL থাকে history, server log আর শেয়ার করা লিংকে। (৩) প্রতিটি অক্ষরে নতুন history entry দিলে Back নষ্ট হয়। সার্চ বক্সে debounce করুন, আর নতুন entry-র বদলে বর্তমান entry-টি replace করুন। (৪) URL লম্বা হওয়ার সীমা আছে। RFC 9110 অন্তত 8000 octet সমর্থনের সুপারিশ করে, তবু বড় form-এর ডেটা URL-এ নয়।

৬ · Server state আলাদা — আর সিদ্ধান্তের ছক

তাকে সাজানো ডালের প্যাকেট গুদামের হিসাবের একটি ছায়া। গুদামে শেষ হয়ে গেলেও তাক তা জানে না। Server থেকে আনা পণ্যের তালিকাও তাই। সেটি আপনার মালিকানায় নেই, অন্য কেউ যেকোনো সময় বদলাতে পারে। তাই একে useState বা global store-এ “আমার state” ভেবে রাখা একটি মৌলিক ভুল। এটি আসলে একটি cache.

প্রশ্নClient stateServer state
মালিকএই ব্রাউজারServer বা database
পাওয়া যায়সাথে সাথেNetwork পেরিয়ে, async
পুরোনো হতে পারে?না, এটিই আসলহ্যাঁ, অন্য কেউ বদলালে
আলাদা চিন্তাকে পড়েloading, error, retry, stale, একই request দুবার
সাধারণ জায়গাuseState, Context, storeTanStack Query, router loader, framework-এর data layer

এই আলাদা করার ফল বড়। অনেক অ্যাপের global store-এর একটি বড় অংশ আসলে server ডেটা। সেটি সরিয়ে দিলে store প্রায়ই ছোট হয়ে যায়। Cache-এর তত্ত্ব আছে System Design কোর্সে, আর বাজারঘরে এর ব্যবহার পাবেন পর্ব ১৯-এ।

এক টুকরো state-এর জায়গা ঠিক করা

এবার সব মিলিয়ে একটি প্রশ্নমালা। ওপর থেকে নিচে যান, প্রথম “হ্যাঁ”-তেই থামুন। নিচে বাজারঘরের category filter-কে এই পথে হাঁটানো হয়েছে।

  1. হিসাব করা যায়?না
  2. মালিক server?না
  3. লিংকে টিকবে?হ্যাঁ
  4. জায়গা: URL?cat=oil
  5. ফল: cachequery key
প্রশ্ন ১অন্য মান থেকে পাওয়া যায়?
selected category = ?   // ব্যবহারকারীর বাছাই
কিছু থেকে হিসাব হয় না, তাই এটি আসল state
প্রশ্ন ২আসল কপি server-এ?
category-র তালিকা      -> server-এর
কোনটি বেছেছেন          -> ব্যবহারকারীর
প্রশ্ন ৩Refresh, Back, শেয়ার
refresh দিলে থাকবে?    হ্যাঁ, থাকা উচিত
লিংকে পাঠানো যাবে?     হ্যাঁ, থাকা উচিত
সিদ্ধান্তuseState নয়, URL
const cat = params.get('cat') ?? 'all';
// যে component-এর লাগবে, সে নিজেই পড়বে
পরের ধাপURL থেকে server cache
URL  ->  cat = oil  ->  GET /api/products?cat=oil
                        ফল থাকে server cache-এ
  1. প্রশ্ন ১: হিসাব করে পাওয়া যায়? না। ব্যবহারকারী কী বেছেছেন, তা অন্য কিছু থেকে বের করা যায় না। তাই এটি আসল state, derived নয়।
  2. প্রশ্ন ২: মালিক কি server? তালিকাটি server-এর, কিন্তু বাছাইটি ব্যবহারকারীর এই মুহূর্তের ইচ্ছা। তাই না।
  3. প্রশ্ন ৩: Refresh, Back বা লিংকে টিকতে হবে? হ্যাঁ। ক্রেতা লিংক পাঠাতে চান। এখানেই থামি।
  4. সিদ্ধান্ত: URL. cat থাকবে query string-এ। আলাদা useState নয়। তালিকা আর breadcrumb, দুজনেই URL পড়বে, তাই prop drilling-ও লাগছে না।
  5. শেষ জোড়া। cat বদলালে নতুন request যায়, আর তার ফল থাকে server cache-এ। অর্থাৎ বাছাই URL-এ, ফল cache-এ। দুই জাত, দুই জায়গা। পর্ব ১৯-এ এই cat-ই query key-র অংশ হবে।
ক্রমপ্রশ্নহ্যাঁ হলে জায়গা
১হিসাব করে পাওয়া যায়?State নয়, render-এ হিসাব
২আসল কপি server-এ?Server cache (পর্ব ১৯)
৩Refresh, Back, শেয়ারে টিকবে?URL (পর্ব ২০)
৪একটিই component পড়ে?Local useState
৫কাছাকাছি কয়েকটি পড়ে?সাধারণ parent-এ তুলুন
৬দূরে, কিন্তু কম বদলায়?Context (পর্ব ১৭)
৭দূরে, আর ঘন ঘন বদলায়?Client store (পর্ব ১৮)

৭ · Prop, Context, Zustand না Redux

State ওপরে উঠে দূরে পৌঁছাতে হলে পথ চারটি। এগুলো শত্রু নয়, একটি সিঁড়ির ধাপ। নিচের ধাপে কাজ চললে ওপরে উঠবেন না। প্রতিটির API-র গভীরে যাবেন পর্ব ১৭ আর ১৮-এ। এখানে শুধু বেছে নেওয়ার যুক্তি।

পথকখন মানায়কে re-render হয়দাম
Props ও compositionদুই-তিন স্তর, পরিষ্কার পথParent-এর নিচের অংশস্তর বাড়লে prop drilling
Contexttheme, ভাষা, লগইন করা ব্যবহারকারীমান বদলালে সব consumerঘন ঘন বদলালে অনেক render
Zustandমাঝারি অ্যাপ, ছোট API চাইযে selector-এর ফল বদলেছেএকটি dependency, নিয়ম নিজেকে ঠিক করতে হয়
Redux Toolkitবড় দল, জটিল ক্লায়েন্ট logic, ঘটনার হিসাব চাইযে selector-এর ফল বদলেছেবেশি কাঠামো, শেখার সময়

লেখার সময়ে Zustand 5.0.16 আর Redux Toolkit 2.13.0. Context কোনো state রাখার যন্ত্র নয়, শুধু মান পৌঁছে দেওয়ার পথ। State থাকে useState বা useReducer-এ। Context-এর বড় কথাটি হলো, Provider-এর মান বদলালে সব consumer আবার render হয়, মানের যে অংশটি সে পড়ে তা না বদলালেও। তাই ঘন ঘন বদলানো state-এ এটি মানায় না।

মতভেদ আছে। কেউ বলেন Redux এখন বাড়তি বোঝা, কেউ বলেন বড় দলে এর কঠোর কাঠামোই সবচেয়ে কাজের। দুই পক্ষেরই যুক্তি আছে। প্রশ্ন “কোনটি সেরা” নয়, প্রশ্ন আপনার অ্যাপে client state কতটা আছে। Server cache আর URL সরানোর পরে যা বাকি থাকে, তার আকারই উত্তর দেয়। অনেক শপে বাকি থাকে শুধু কার্ট আর theme.

৮ · State machine — অসম্ভব অবস্থা বাদ দিন

Checkout-এ এমন state প্রায়ই দেখা যায়: isLoading, isPaid, hasError. তিনটি boolean মানে ২×২×২ = ৮টি সম্ভাব্য মিলন। অথচ বৈধ অবস্থা মাত্র কয়েকটি। isLoading আর isPaid একসাথে সত্য হলে পর্দায় কী দেখাবে? এমন impossible state ঠেকানোর দায় আপনার ওপর পড়ে। React docs-ও এজন্য বেশ কয়েকটি boolean-এর বদলে একটি status রাখতে বলে।

ঢাকার মেট্রোর গেট ভাবুন। কার্ড ছোঁয়ালে “বন্ধ” থেকে “খোলা”, পার হলে আবার “বন্ধ”। “খোলা” অবস্থায় আবার ছোঁয়ালে কিছু হয় না। State machine হলো ঠিক এই ধারণা: নির্দিষ্ট কয়েকটি অবস্থা, প্রতিটিতে নির্দিষ্ট কিছু ঘটনা, আর একটি টেবিল। টেবিলে না থাকলে ঘটনাটি উপেক্ষিত। নিচে বাজারঘরের checkout. দুবার “Pay” চাপলে কী হয়, দেখুন।

1cart:    { NEXT: address }2address: { NEXT: payment, BACK: cart }3payment: { PAY: paying, BACK: address }4paying:  { OK: success, FAIL: failed }5failed:  { RETRY: payment }6success: { }
এই ধাপ
এখনকার অবস্থা
ঘটনা
টেবিলে আছে?
পরের অবস্থা
  1. cart + NEXT. ক্রেতা কার্ট দেখে এগোলেন। টেবিলে cart সারিতে NEXT আছে, তাই অবস্থা হলো address.
  2. address + NEXT. ঠিকানা দেওয়া হলো। অবস্থা হলো payment.
  3. payment + PAY. Pay চাপলেন। অবস্থা হলো paying. এখানেই gateway-র কাছে অনুরোধ যাবে।
  4. paying + PAY. ধীর নেটওয়ার্কে ক্রেতা অধৈর্য হয়ে আবার চাপলেন। paying সারিতে PAY নেই, তাই কিছুই হলো না। অবস্থা paying-ই রইল। দ্বিতীয় পেমেন্ট চেষ্টা এই পথে শুরু হয় না।
  5. paying + FAIL. Gateway ব্যর্থতা জানাল। অবস্থা হলো failed.
  6. failed + RETRY. ক্রেতা আবার চেষ্টা বেছে নিলেন। অবস্থা ফিরল payment-এ।
  7. payment + PAY. আবার Pay. অবস্থা paying.
  8. paying + OK. সফল। অবস্থা success. এই সারি ফাঁকা, তাই আর কোনো ঘটনা এখান থেকে অবস্থা বদলাবে না।

এবার একই টেবিল সত্যিকারের React-এ, মাত্র একটি useState আর একটি pure function দিয়ে। কোনো library নেই। Render-এর শর্তগুলো দেখুন, boolean-এর জট নেই।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const machine = {                 // কোন অবস্থায় কোন ঘটনা বৈধ, পুরোটা এক টেবিলে
  cart:    { NEXT: 'address' },
  address: { NEXT: 'payment', BACK: 'cart' },
  payment: { PAY: 'paying', BACK: 'address' },
  paying:  { OK: 'success', FAIL: 'failed' },
  failed:  { RETRY: 'payment' },
  success: {},
};
const events = ['NEXT', 'BACK', 'PAY', 'OK', 'FAIL', 'RETRY'];
// pure function: টেবিলে না থাকলে অবস্থা একই
const step = (state, event) => machine[state][event] ?? state;
function Checkout() {
  const [state, setState] = useState('cart');
  function send(event) {
    const to = step(state, event);
    console.log(state, '+', event, '->', to, to === state ? '(ignored)' : '');
    setState(to);
  }
  return <div><p>অবস্থা: <b>{state}</b></p>
    {events.map(e => <button key={e} onClick={() => send(e)}>{e}</button>)}
    {state === 'paying' && <p>পেমেন্ট হচ্ছে, অপেক্ষা করুন…</p>}
    {state === 'failed' && <p>পেমেন্ট হয়নি। আবার চেষ্টা করুন।</p>}
    {state === 'success' && <p>অর্ডার হয়েছে।</p>}</div>;
}
console.log('start: cart');
createRoot(document.getElementById('root')).render(<Checkout />);
</script>

Edge-এ চালিয়ে PAY, NEXT, NEXT, PAY, PAY, BACK, FAIL, RETRY, PAY, OK, NEXT চাপলে যা এলো:

start: cart
cart + PAY -> cart (ignored)
cart + NEXT -> address
address + NEXT -> payment
payment + PAY -> paying
paying + PAY -> paying (ignored)
paying + BACK -> paying (ignored)
paying + FAIL -> failed
failed + RETRY -> payment
payment + PAY -> paying
paying + OK -> success
success + NEXT -> success (ignored)

শুরুতেই cart-এ PAY উপেক্ষিত, মানে কার্ট থেকে সরাসরি পেমেন্ট সম্ভব নয়। paying-এর দ্বিতীয় PAY আর BACK-ও উপেক্ষিত। শেষের NEXT-ও, কারণ success সারি ফাঁকা। পর্দার বোতাম বন্ধ করা ছাড়াই নিয়মটি এক জায়গায় ধরা আছে।

ছোট টেবিলে এটুকুই যথেষ্ট। বড় flow-তে, যেমন সময়সীমা, একসাথে চলা অংশ, আর আগে থেকে চিত্র আঁকা দরকার হলে XState-এর মতো library কাজে আসতে পারে। তবে এটি নতুন একটি dependency আর শেখার বিষয়, তাই সাধারণ form-এ সাধারণত দরকার পড়ে না। নিচে একই machine XState 5.33.3-এ, Node-এ সত্যিকারের run (node checkout.mjs).

import { createMachine, createActor } from 'xstate';

const checkout = createMachine({
  id: 'checkout',
  initial: 'cart',
  states: {
    cart:    { on: { NEXT: 'address' } },
    address: { on: { NEXT: 'payment', BACK: 'cart' } },
    payment: { on: { PAY: 'paying', BACK: 'address' } },
    paying:  { on: { OK: 'success', FAIL: 'failed' } },
    failed:  { on: { RETRY: 'payment' } },
    success: { type: 'final' },
  },
});
const actor = createActor(checkout);
actor.start();
// ... প্রতিটি ঘটনা actor.send({ type }) দিয়ে পাঠিয়ে ছাপা হলো
cart     + NEXT  -> address
address  + NEXT  -> payment
payment  + PAY   -> paying
paying   + PAY   -> paying  (ignored)
paying   + FAIL  -> failed
failed   + RETRY -> payment
payment  + PAY   -> paying
paying   + OK    -> success
status: done

ওপরের stepper-এর আট ধাপ আর এই আট লাইন হুবহু মেলে। শেষের status: done এসেছে type: 'final' থেকে, যা আমাদের হাতে লেখা টেবিলে ছিল না।

Machine শুধু এই ব্রাউজারের অবস্থা সামলায়। দুই tab থেকে Pay চাপলে বা request দুবার গেলে সেটি ঠেকানো server-এর কাজ, idempotency দিয়ে। পেমেন্টের সেই অংশ আসবে পর্ব ৩৩ আর ৬৫-এ।

৯ · localStorage-এর ঝুঁকি আর বাজারঘরের পুরো মানচিত্র

Guest কার্ট tab বন্ধের পরও রাখতে চাইলে সবচেয়ে সহজ পথ localStorage. পর্ব ১৪-এ useLocalStorage লিখেছেন, আর API-র বিস্তার আছে JavaScript কোর্সে। কিন্তু persistence মানেই state এখন আপনার কোডের নতুন version-এর চেয়ে বেশি দিন বাঁচে। সবচেয়ে চুপচাপ ঝুঁকিটি এখানেই। ধরুন পুরোনো release কার্ট রেখেছিল ['rice', 'dal'] আকারে। নতুন কোড চায় { id, qty, price }. নিচে Zustand-এর persist দিয়ে সত্যিকারের run, Node-এ, Map-ভিত্তিক একটি নকল storage-এ।

const total = (s) => s.items.reduce((sum, i) => sum + i.qty * i.price, 0);

// ... naive অংশ আর ছাপার লাইন এখানে ছোট করা হলো
// ২: version 2, migrate পুরোনো আকারকে নতুন আকারে আনে
const safe = createStore()(persist(() => ({ items: [] }), {
  name: 'cart', version: 2, storage: createJSONStorage(() => memory),
  migrate: (old, from) => from < 2
    ? { items: old.items.map((id) => ({ id, qty: 1, price: 0 })) }
    : old,
}));
$ node persist.mjs
naive items: ["rice","dal"]
naive total: NaN
migrate from version 1
safe items: [{"id":"rice","qty":1,"price":0},{"id":"dal","qty":1,"price":0}]
safe total: 0
saved: {"state":{"items":[{"id":"rice","qty":1,"price":0},{"id":"dal","qty":1,"price":0}]},"version":2}

naive অংশে version বদলানো হয়নি, migrate-ও নেই। কোনো error আসেনি, পর্দায় শুধু মোট NaN. Zustand-এর docs-ও জানায়, createJSONStorage পড়ার সময় ডেটা যাচাই করে না। safe-এ version বাড়িয়ে পুরোনো আকারকে নতুন আকারে আনা হয়েছে। price: 0 দেখে অবাক হবেন না। দাম এখানে রাখা হয়নি, কারণ দামের মালিক server. বাকি ঝুঁকিগুলো এক নজরে:

  • গোপন তথ্য নয়: একই origin-এর যেকোনো script localStorage পড়তে পারে। তাই XSS হলে সবই বেরিয়ে যায়। Token রাখার আলোচনা আছে Cybersecurity কোর্সে আর পর্ব ৪৬-এ।
  • বিশ্বাস নয়: ব্যবহারকারী DevTools-এ মান বদলাতে পারেন। দাম, ছাড় বা stock সব server আবার হিসাব করবে।
  • Server-এ নেই: SSR-এর সময় localStorage থাকে না, তাই প্রথম HTML আর ব্রাউজারের মান না মিললে hydration mismatch হতে পারে (পর্ব ৫৬)।
  • সীমা ও ব্যর্থতা: শুধু string রাখে, কাজ করে synchronous-ভাবে, জায়গা সীমিত, আর setItem error ছুড়তে পারে। তাই try/catch রাখুন।
  • একাধিক tab আর শেয়ার করা কম্পিউটার: অন্য tab-এর বদল জানতে storage event শুনতে হয়। Logout-এ ব্যক্তিগত ডেটা মুছুন।

বাজারঘরের state মানচিত্র

এবার পুরো ছবি। প্রতিটি তথ্য নিজের জায়গায়, আর একটি তথ্যের একটিই আসল ঠিকানা।

বাজারঘরের state মানচিত্র

Every piece of state has one home

বাজারঘরের পাঁচ রকম state কোথায় থাকে ব্রাউজার · Browser URL state filter · sort · page · ?cat=oil Local component state modal · কুপন ঘর · সার্চের লেখা Client store guest কার্ট: id, qty theme localStorage persist কপি Server cache products · দাম · /me · কার্ট (লগইন) fetch mutation Server · আসল কপি database · stock · order · session session id থাকে httpOnly cookie-তে
ছবি ১৬.১ — ওপর থেকে নিচে, স্বল্পজীবী থেকে দীর্ঘজীবী। আসল কপি সবার নিচে।
তথ্যজাতজায়গাকারণ
পণ্য, দাম, stockServerServer cacheমালিক server, বদলাতে পারে অন্যরা
Filter, sort, pageURLQuery stringশেয়ার, Back, refresh
কার্ট (guest)Client globalStore + localStorageদূরের Badge আর Checkout পড়ে; শুধু id, qty
কার্ট (লগইন)ServerServer cacheএকাধিক ডিভাইসে একই কার্ট; লগইনে guest কার্ট মেলাতে হয়
SessionServerhttpOnly cookie, /me cacheJavaScript cookie পড়তে পারে না; নাম-ছবি cache-এ
ModalUILocal useStateশুধু ওই পাতার; লিংকে খোলা চাইলে URL
কুপন ঘরFormLocalSubmit-এর আগে আর কেউ পড়ে না
মোট বিলDerivedকোথাও নয়Render-এ হিসাব, চূড়ান্ত হিসাব server-এ
Checkout-এর ধাপUIState machineঅসম্ভব অবস্থা বাদ
ThemeClient globalContext + localStorageকম বদলায়, দূরে লাগে

১০ · AI-এর যুগে state-এর নকশা

AI একটি useCartStore কয়েক সেকেন্ডে লিখে দেয়, কোডটি চলেও। কিন্তু “কোথায় রাখবেন” প্রশ্নটি অ্যাপের জীবনচক্রের, আর AI আপনার অ্যাপের পুরো ছবি দেখে না। তাই সাধারণ প্রশ্নে সে পরিচিত নকশাই দেয়।

AI-এর ভুলকেন সমস্যাধরার উপায়
সব কিছু এক global store-একুপন টাইপে দূরের অংশও render হতে পারেওপরের ছকে প্রতিটি মান মিলিয়ে দেখা
Server ডেটা useEffect দিয়ে store-এ কপিপুরোনো ডেটা, race conditionপর্ব ১৯-এর পথ জিজ্ঞেস করা
Token localStorage-এXSS হলে পুরো account ঝুঁকিতেStorage খুলে দেখা, পর্ব ৪৫-৪৬
total আলাদা stateনীরব অমিল“এটি হিসাব করা যায়?” জিজ্ঞেস করা
Filter শুধু useState-এRefresh-এ হারায়, লিংক কাজ করে নাRefresh দিয়ে পরীক্ষা
অপ্রয়োজনীয় বা নকল packageবাড়তি dependency, সরবরাহ-শৃঙ্খলের ঝুঁকিnpm view আর অফিসিয়াল docs
পুরোনো Redux ধাঁচহাতে লেখা action type আর লম্বা switchRedux Toolkit-এর বর্তমান docs মেলানো

কোড চাওয়ার আগে নকশা চান, আর AI-কে ছকটি ধরিয়ে দিন। তখন সে প্রতিটি মানের জন্য কারণ দেখাতে বাধ্য হবে, যা আপনি যাচাই করতে পারবেন।

আমি React 19 দিয়ে একটি অনলাইন শপ বানাচ্ছি। নিচে state-এর তালিকা দিলাম: [এখানে আপনার তালিকা দিন] প্রতিটির জন্য একটি টেবিল বানাও: জাত (UI, form, URL, client global, server cache, derived), মালিক, কে কে পড়ে, কতক্ষণ বাঁচবে, আর জায়গা। নিয়ম: ১. হিসাব করে পাওয়া গেলে state নয়, বলো। ২. আগে local আর URL ভাবো, global store শেষে। ৩. কোনো গোপন মান localStorage-এ রাখবে না। ৪. নতুন package লাগলে কেন লাগবে বলো, আর package ছাড়া বিকল্পও বলো। ৫. এখনই কোড লিখবে না।

AI-এর দেওয়া state নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে: Colocation কী? State-কে যার দরকার তার যত কাছে সম্ভব রাখা। Derived state কেন জমাবেন না? দুই কপি একদিন অমিল হয়, হিসাব করলে সবসময় ঠিক। Client আর server state-এর পার্থক্য? Server state-এর মালিক অন্য জায়গায়, তাই সেটি পুরোনো হতে পারে, এটি একটি cache. Context কি state management? না, এটি মান পৌঁছানোর পথ; মান বদলালে সব consumer render হয়। Filter URL-এ কেন? Refresh, Back আর শেয়ার করা লিংকে অবস্থা টিকে থাকে।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১একসময় প্রায় সব React অ্যাপে Redux থাকত। এখন অনেক অ্যাপে থাকে না কেন? Redux কি ভুল ছিল?

ভুল ছিল না, সময়ের প্রয়োজন ছিল। ২০১৪ সালে Facebook-এর দল Flux নকশা তুলে ধরে: ডেটা এক দিকে চলবে, action থেকে store হয়ে view-তে। ২০১৫ সালে Dan Abramov আর Andrew Clark তৈরি করেন Redux, যা ধারণাটিকে এক store আর pure reducer-এ নামিয়ে আনে। তখন React-এ hook ছিল না, Context-এর API-ও আজকের মতো সহজ ছিল না, আর server ডেটা রাখার ভালো জায়গাও ছিল না। তাই সবকিছু Redux-এ যেত, এমনকি একটি input-এর লেখাও।

এরপর তিনটি জিনিস বদলাল।

  • Hook আর নতুন Context: ছোট শেয়ার করা state-এর জন্য বাইরের library কম দরকার হলো। Context-এর বর্তমান API এসেছে React 16.3-তে, hook 16.8-তে।
  • Server cache library: React Query-র মতো টুল দেখাল, store-এর বড় অংশ আসলে server ডেটার হাতে লেখা cache.
  • ছোট store: Zustand-এর মতো library কম কোডে selector-ভিত্তিক সুবিধা দিল।

Dan Abramov নিজেই ২০১৬ সালে “You Might Not Need Redux” লিখেছিলেন। আবার Redux দলও পুরোনো হাতে লেখা ধাঁচ ছেড়ে Redux Toolkit-কে সুপারিশ করে, যা boilerplate অনেক কমিয়েছে। বড় দলে যেখানে প্রতিটি পরিবর্তনের হিসাব, কঠোর নিয়ম আর DevTools-এ ধাপে ধাপে ফিরে দেখা দরকার, সেখানে এটি আজও কাজের। কেউ বলেন এত কাঠামো বেশিরভাগ অ্যাপে অকারণ, কেউ বলেন এই কাঠামোই বড় দলকে বাঁচায়। দুটোই পরিস্থিতিভেদে সত্য।

মূল উপলব্ধি: টুল জনপ্রিয় হয় একটি সমস্যার উত্তরে। সমস্যা অন্য টুলে সরে গেলে জনপ্রিয়তাও কমে, যদিও টুলটি খারাপ হয়নি। আগে সমস্যা মাপুন, তারপর টুল ধরুন।

প্র ০২State machine-এর ধারণা তো ১৯৫০-এর দশকের। ওয়েব UI-তে এটি এত দেরিতে এলো কেন, আর এটি সব জায়গায় ব্যবহার করা উচিত কি?

Finite state machine-এর গাণিতিক ভিত্তি অনেক পুরোনো। ১৯৫০-এর দশকে George Mealy আর Edward Moore দুটি সুপরিচিত রূপ দেন। সেগুলো ডিজিটাল সার্কিট, compiler আর network protocol-এ বহুদিন ধরেই মূল হাতিয়ার। TCP-র সংযোগের অবস্থাগুলোও একটি state machine দিয়েই বর্ণনা করা হয়।

সমস্যা হলো, বড় UI-তে অবস্থার সংখ্যা বিস্ফোরিত হয়। ১৯৮৭ সালে David Harel “Statecharts” নামের একটি প্রবন্ধে এর সমাধান দেন: অবস্থার ভেতরে অবস্থা, একসাথে চলা অংশ আর ইতিহাস। উড়োজাহাজের জটিল সিস্টেমের কাজ থেকেই এই ধারণা আসে। পরে W3C-ও SCXML নামে একটি মান প্রকাশ করে। ওয়েবে দীর্ঘদিন UI ছিল সরল, আর boolean দিয়েই কাজ চলত। Checkout, ফাইল আপলোড আর video call-এর মতো জটিল flow ব্রাউজারে আসার পর বিষয়টি জরুরি হলো। XState-এর মতো library, যার পেছনে আছেন David Khourshid, এই ধারণাকে JavaScript-এ জনপ্রিয় করে।

সব জায়গায় ব্যবহার করা উচিত কি? না। একটি modal-এর খোলা-বন্ধের জন্য একটি boolean-ই যথেষ্ট। দামটি হলো নতুন ধারণা, আরও কোড, আর দলের সবার শেখা। লাভ আসে তখনই, যখন তিনটি লক্ষণ দেখা যায়: কয়েকটি boolean মিলে অবস্থা বোঝাচ্ছে, “এই অবস্থায় এটি হওয়া উচিত নয়” ধরনের if ছড়িয়ে আছে, আর ধাপগুলো নিয়ে দলের মধ্যে বিভ্রান্তি আছে। মাঝের পথও আছে: এই পর্বের মতো একটি ছোট টেবিল, অথবা একটি status string. React docs-এর পরামর্শও প্রথমে এটুকুই।

মূল উপলব্ধি: ভালো state নকশা মানে ভুল অবস্থাকে প্রকাশ করাই অসম্ভব করে তোলা। সেটি library দিয়েও হয়, শুধু একটি ভালো টেবিল দিয়েও হয়।

প্র ০৩“Server state আসলে cache” — এই একটি বাক্য কেন frontend-এর নকশা এত বদলে দিল?

আগের অভ্যাস ছিল এমন: Effect-এ fetch, ফল store-এ, সাথে isLoading আর error. প্রতিটি তালিকার জন্য একই কোড বারবার। সমস্যাগুলো ধীরে ধীরে দেখা দিত। দুটি component একই ডেটা চাইলে দুবার request যায়। ব্যবহারকারী পাতা বদলে ফিরে এলে আবার loading দেখা যায়। আরেক tab-এ দাম বদলালে এখানে পুরোনো দাম থেকে যায়। পুরোনো request দেরিতে ফিরে নতুন ফলের ওপর বসে পড়ে, যা পর্ব ১৪-এ race condition হিসেবে দেখেছেন।

এগুলো কোনো আলাদা bug নয়। এগুলো সবই cache-এর পরিচিত সমস্যা: কখন পুরোনো ধরবেন, কখন আবার আনবেন, এক চাবিতে একই ফল ভাগ করবেন কীভাবে। Phil Karlton-এর নামে প্রচলিত একটি বিখ্যাত উক্তি আছে, কম্পিউটার বিজ্ঞানের দুটি কঠিন কাজের একটি হলো cache invalidation. ফলে হাতে লেখা fetch মানে আসলে না জেনেই একটি কাঁচা cache বানানো।

২০১৯-এর দিকে Tanner Linsley-র React Query আর Vercel-এর SWR এই সমস্যাকে নাম ধরে সামনে আনে। SWR নামটি এসেছে HTTP-র stale-while-revalidate নিয়ম থেকে, যা RFC 5861-এ আছে: পুরোনো ফল এখনই দেখানো, পেছনে নতুন আনা। নকশার বদলটি বড়। Component আর নিজে ডেটা আনে না, শুধু জানায় “আমার এই চাবির ডেটা লাগবে”। বাকি কাজ cache-এর।

এর একটি ভালো পার্শ্বফল হলো, store ছোট হয়। Kent C. Dodds-এর মতো লেখকেরাও বহুদিন ধরে বলে আসছেন, ডেটা আনা আর UI state আলাদা করলে অ্যাপের জটিলতা অনেক কমে। React-এর নিজের docs-ও Effect-এ হাতে fetch করার বদলে এমন ব্যবস্থার পরামর্শ দেয়। তবে সীমাও আছে। Offline-first অ্যাপে ক্লায়েন্টের কপিই অনেক সময় প্রধান হয়ে ওঠে, তখন sync-এর আলাদা নকশা লাগে।

মূল উপলব্ধি: একটি সমস্যার সঠিক নাম খুঁজে পেলে অর্ধেক সমাধান হয়ে যায়। “আমার state” না বলে “অন্যের ডেটার কপি” বললেই পুরোনো হওয়া, আবার আনা, ভাগ করার প্রশ্ন নিজে থেকে সামনে আসে।

প্র ০৪Web-এর শুরুতে URL-ই ছিল অ্যাপের state. SPA এসে সেটি হারাল কেন, আর এখন আবার URL-এ ফেরার চেষ্টা কেন?

Web-এর মূল নকশায় প্রতিটি পাতার একটি ঠিকানা ছিল। Roy Fielding-এর ২০০০ সালের REST গবেষণাপত্রে “application state” চলে hypermedia ধরে, মানে লিংক ধরে। Tim Berners-Lee ১৯৯৮ সালে “Cool URIs don't change” লেখায় বলেন, ঠিকানা স্থায়ী হওয়া উচিত। Back বোতাম, bookmark আর লিংক শেয়ার সবই এই নকশার উপহার।

Ajax আর SPA এসে পাতা না বদলেই পর্দা বদলানো শুরু করল। অবস্থা চলে গেল JavaScript-এর ভেরিয়েবলে। ফল হলো চেনা সমস্যা: refresh-এ সব হারায়, Back অ্যাপ থেকে বের করে দেয়, লিংকে শুধু হোমপেজ খোলে, আর search engine ভেতরের পাতা দেখে না। প্রথমে # চিহ্ন দিয়ে কৌশল চলত, কারণ fragment বদলালে পাতা reload হয় না। পরে HTML5-এর History API (pushState) সত্যিকারের URL বদলানোর পথ দিল, আর সেই ভিত্তিতেই আজকের client-side router দাঁড়িয়ে আছে।

এখন প্রবণতা আবার URL-এর দিকে, কারণ সে বিনামূল্যে অনেক কিছু দেয়। SSR-এ server নিজেই URL পড়ে সঠিক HTML পাঠাতে পারে, যা useState-এর filter পারে না। পর্ব ৫৩-এ দেখবেন, পাতার ডেটা URL থেকেই আসছে। শেয়ার করা লিংকে দুজন একই অবস্থা দেখেন, যা support-এর কাজও সহজ করে।

দাম আছে তিনটি। URL সবার চোখে পড়ে, তাই নকশা পরিচ্ছন্ন রাখতে হয়। মান সবসময় string, তাই পড়ার সময় রূপান্তর আর যাচাই লাগে। আর URL-এর তথ্য history, log আর Referer header-এ যেতে পারে, তাই RFC 9110-ও সংবেদনশীল তথ্য URI-তে রাখার ঝুঁকি নিয়ে সতর্ক করে। Modal, hover বা ক্ষণস্থায়ী অবস্থা URL-এ রাখলে আবার Back বোতামই বিরক্তিকর হয়ে ওঠে।

মূল উপলব্ধি: “এই অবস্থাটি কি একটি লিংকের মধ্যে অর্থপূর্ণ?” হ্যাঁ হলে সেটি URL-এর। পুরোনো web-এর এই সহজ নকশা আজও অনেক ফ্রন্টএন্ড সমস্যার সবচেয়ে সস্তা সমাধান।

প্র ০৫কার্ট কি localStorage-এ রাখা উচিত, নাকি server-এ? সঠিক উত্তর কীভাবে ঠিক করবেন?

কোনো একটি উত্তর সব শপে খাটে না। এটি একটি ভালো trade-off-এর প্রশ্ন। localStorage এসেছে HTML5-এর Web Storage-এর অংশ হিসেবে, cookie-র চেয়ে বেশি ডেটা রাখার জন্য। প্রতিটি request-এ এটি server-এ যায় না, তাই খরচ কম। ব্রাউজারভেদে জায়গার সীমা আলাদা, সাধারণত প্রতি origin-এ কয়েক মেগাবাইট।

  • localStorage-এর পক্ষে: লগইন ছাড়াই কাজ করে, server-এ চাপ নেই, দ্রুত দেখায়। নতুন ক্রেতার জন্য লগইন ছাড়া কার্ট অনেক সময় সুবিধাজনক।
  • এর বিপক্ষে: এক ডিভাইসেই সীমাবদ্ধ। ফোনে রাখা কার্ট কম্পিউটারে নেই। পুরোনো দাম জমে থাকে। ব্যবহারকারী চাইলে মান বদলাতে পারেন। কিছু ব্রাউজার প্রাইভেসির কারণে সংরক্ষণের মেয়াদ সীমিত করতে পারে; WebKit দলের ২০২০ সালের একটি ঘোষণায় script দিয়ে লেখা storage-এ সাত দিনের সীমার কথা ছিল, বিশদ আপনার লক্ষ্য ব্রাউজারের বর্তমান নথিতে দেখে নিন।
  • Server-এর পক্ষে: সব ডিভাইসে একই কার্ট, ছেড়ে যাওয়া কার্ট বিশ্লেষণ, আর stock-এর সাথে মিলিয়ে দেখা যায়।
  • এর দাম: প্রতিটি পরিবর্তনে request, লগইন নেই এমন ক্রেতার জন্য আলাদা পরিচয়, আর পুরোনো কার্ট মুছে ফেলার কাজ।

অনেক শপ তাই মিশ্র পথ নেয়। Guest অবস্থায় client-এ, শুধু id আর পরিমাণ। লগইন হলে server-এর কার্টের সাথে মেলানো, আর তখন থেকে server-ই মালিক। মেলানোর নিয়ম আগে থেকে ঠিক করতে হয়: একই পণ্য দুই জায়গায় থাকলে পরিমাণ যোগ হবে, নাকি বড়টি থাকবে? দুটি কথা অপরিবর্তনীয়। Checkout-এর দাম, ছাড় আর stock সবসময় server হিসাব করবে। আর পেমেন্টের তথ্য কখনো localStorage-এ নয়।

মূল উপলব্ধি: “কোথায় রাখব” প্রশ্নের নিচে আছে আরও দুটি প্রশ্ন: কে বিশ্বাসযোগ্য, আর ব্যবহারকারীর অভিজ্ঞতা কতদূর টিকতে হবে। ক্লায়েন্টের কপি সুবিধার জন্য, সত্যের জন্য নয়।

অনুশীলন

খাতা-কলম আর ওপরের live ব্লকগুলোই এখানে যথেষ্ট।

  1. Render হিসাব (numerical): একটি পাতায় App-এর নিচে Header, Filter, List, আর List-এর নিচে ১৫০টি Item. State আছে App-এ, কোনো memo নেই। (ক) এক অক্ষরে কতটি render? (খ) ৮ অক্ষরে কতটি? (গ) প্রতিটি অনুমিত 0.08 ms হলে এক অক্ষরে কত সময়, আর 16.7 ms থেকে কত বাকি? (ঘ) State Filter-এ নামালে ৮ অক্ষরে কতটি?

    (ক) 1 + 3 + 150 = 154টি। (খ) 8 × 154 = 1232টি। (গ) 154 × 0.08 = 12.32 ms, বাকি 16.7 − 12.32 = 4.38 ms, যা style, layout আর paint-এর জন্য বেশ কম। (ঘ) শুধু Filter render হয়: 8 × 1 = 8টি। শর্ত একটিই, List-এর এই লেখাটি দরকার থাকা চলবে না। দরকার থাকলে এত নিচে নামানো যাবে না, তখন useDeferredValue-এর মতো পথ ভাবতে হবে।

  2. নকশা সমালোচনা: একটি শপের একটিমাত্র global store-এ আছে: products, isModalOpen, couponInput, authToken, cartItems, cartTotal, currentPage, theme. পুরো store localStorage-এ সংরক্ষিত। প্রতিটির সঠিক জায়গা বলুন, আর সবচেয়ে বিপজ্জনক ভুলটি চিহ্নিত করুন।

    products: server cache. isModalOpen আর couponInput: local state. currentPage: URL. cartTotal: কোথাও নয়, হিসাব করুন। cartItems আর theme: client global-এ থাকতে পারে, সংরক্ষণও চলে। শুধু id আর পরিমাণ রাখাই ভালো।

    সবচেয়ে বিপজ্জনক: authToken সরাসরি localStorage-এ। XSS হলে যেকোনো script সেটি পড়তে পারে। Session id httpOnly cookie-তে রাখার পথ দেখুন পর্ব ৪৫-এ। দ্বিতীয় বড় ভুল পুরো store সংরক্ষণ করা। Zustand-এ partialize দিয়ে শুধু দরকারি অংশ বেছে নেওয়া যায়।

  3. ফল আগে বলুন (predict): এই টেবিল ধরুন: cart {NEXT→address}, address {NEXT→payment, BACK→cart}, payment {PAY→paying, BACK→address}, paying {OK→success, FAIL→failed}, failed {RETRY→payment}, success {}. শুরু cart. ঘটনা: PAY, NEXT, BACK, NEXT, NEXT, PAY, FAIL, PAY, RETRY. শেষ অবস্থা কী, আর কয়টি ঘটনা উপেক্ষিত?

    cart (PAY উপেক্ষিত) → address → cart → address → payment → paying → failed (PAY উপেক্ষিত) → payment. শেষ অবস্থা payment, উপেক্ষিত ২টি। checkout.html ব্লকে একই ক্রমে চেপে মিলিয়ে নিন।

  4. URL শক্ত করুন (কোড): url-filters.html ব্লকে start বদলে করুন /products?cat=rice&page=abc. কী দেখছেন? তারপর এক লাইন বদলে এমন করুন, যাতে অবৈধ page হলে 1 ধরা হয়।

    Number('abc') হয় NaN, তাই slice ঠিকমতো কাজ করে না আর তালিকা খালি থাকে। সমাধান:

    const page = Math.max(1, Math.floor(Number(query.get('page'))) || 1);

    NaN আর 0 falsy, তাই || 1 কাজে আসে। ঋণাত্মক মান Math.max সামলায়। URL সবসময় ব্যবহারকারীর ইনপুট, তাই server-ও একই যাচাই করবে (পর্ব ৩৪)।

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

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

ক্রমএক টুকরো state-এর জায়গা ঠিক করার প্রশ্নগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. হিসাব করে পাওয়া যায় কি?
  2. আসল কপি কি server-এ?
  3. Refresh, Back বা লিংকে টিকতে হবে কি?
  4. একটিই component কি এটি পড়ে?
  5. কাছাকাছি কয়েকটি component পড়ে? তাহলে সাধারণ parent
  6. অনেক দূরে লাগে? তাহলে Context বা store

React অ্যাপে state colocation বলতে কী বোঝায়?

  • সব state এক global store-এ রাখা
  • State-কে তার ব্যবহারকারী component-এর যত কাছে সম্ভব রাখা
  • State সবসময় root component-এ তুলে রাখা
  • State-এর সব ফাইল একই ফোল্ডারে রাখা

একটি কার্টে items state আছে, আর মোট দাম আছে আলাদা total state-এ। সেরা সংশোধন কোনটি?

  • useEffect দিয়ে items বদলালে total বদলানো
  • total-কে Context-এ সরানো
  • total state মুছে render-এর সময় items থেকে হিসাব করা
  • total-কে localStorage-এ সংরক্ষণ করা

একটি পণ্য তালিকার filter বন্ধুকে লিংকে পাঠানো গেলে ভালো হয়। Filter কোথায় রাখা উচিত?

  • Component-এর useState-এ
  • Zustand store-এ
  • localStorage-এ
  • URL-এর query string-এ

Context-এর Provider-এর মান ঘন ঘন বদলালে সাধারণত কী সমস্যা হয়?

  • সেই context পড়া সব component আবার render হয়
  • React একটি error দেখায়
  • মান server-এ চলে যায়
  • মান localStorage-এ জমতে থাকে

Server থেকে আনা পণ্যের তালিকাকে “state” না বলে “cache” বলা হয় কেন?

  • কারণ ডেটা সবসময় বড়
  • কারণ আসল কপি অন্য জায়গায়, আর এই কপি পুরোনো হয়ে যেতে পারে
  • কারণ React এটি নিজে থেকে মুছে দেয়
  • কারণ এটি সবসময় হার্ডডিস্কে থাকে

একটি checkout-এ isLoading, isPaid, hasError নামে তিনটি boolean আছে। Machine-এর একটি status-এ বদলালে প্রধান লাভ কী?

  • অ্যাপ অনেক দ্রুত চলে
  • Server-এ পেমেন্ট যাচাই আর লাগে না
  • অর্থহীন মিলন, যেমন একসাথে loading আর paid, প্রকাশ করাই যায় না
  • UI লেখার আর দরকার হয় না

শব্দকোষ

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

মনে রাখুন

  1. State পাঁচ জাতের: UI, form, URL, client global, server cache. আগে জাত চিনুন।
  2. যতটা নিচে সম্ভব রাখুন, দরকারে তুলুন। State-এর মালিকের নিচের সবাই render হয়।
  3. হিসাব করা যায় এমন কিছু জমাবেন না। এক তথ্য, এক ঠিকানা।
  4. শেয়ারযোগ্য অবস্থা URL-এ, server-এর ডেটা cache-এ। Context আর store আসে এর পরে।
  5. অবৈধ অবস্থা বাদ দিতে একটি status বা machine; localStorage-এ কখনো গোপন তথ্য নয়, আর আকার বদলালে migrate.

পরের পর্বে: ছকের ৬ নম্বর ধাপ। Theme আর লগইন করা ব্যবহারকারীকে দূরের component-এ পৌঁছাবেন Context দিয়ে, আর জটিল আপডেট গুছাবেন useReducer-এ।

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

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