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

State আর event — useState, snapshot আর re-render

State and Events — useState, Snapshots and Re-rendering

Taught in Bangla, this lesson explains why a plain variable does not update the screen, useState as React's ledger, state as a snapshot per render, batching and updater functions, passing rather than calling event handlers, updating objects and arrays without mutation, not storing what can be computed, lifting shared state to the nearest parent, when state resets by position and key, and why code that seems to work still needs checking.

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

  • একটি ক্লিক থেকে স্ক্রিন বদলানো পর্যন্ত পুরো পথ: event, render আর commit
  • Snapshot, batching আর updater function দিয়ে আউটপুট আগেই বলে দেওয়া
  • Event handler-এ function পাঠানো, preventDefault আর stopPropagation
  • বাজারঘরের কার্ট: add, remove আর quantity, state না ভেঙে
  • State-এর গঠন, state ওপরে তোলা, আর state কখন মুছে যায়

১ · সাধারণ variable কেন স্ক্রিন বদলায় না

পর্ব ০৭-এ আপনি component, পর্ব ০৮-এ JSX আর পর্ব ০৯-এ props দেখেছেন। কিন্তু এখনো সবকিছু স্থির। বাজারঘরের “কার্টে যোগ করুন” বোতামে চাপ দিলে কিছু বদলাবে কীভাবে? JavaScript জানা মানুষের প্রথম চেষ্টা হয় এরকম: একটি let variable নিন, ক্লিকে বাড়িয়ে দিন। চলুন সেটিই পাশাপাশি চালিয়ে দেখি।

নিচের ব্লকে দুটি বোতাম আছে। “চালান” চেপে আগে plain +1 তিনবার চাপুন, তারপর state +1 একবার। নিচের log খেয়াল করুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Counter() {
  let plain = 0;
  const [count, setCount] = useState(0);
  console.log(`render: plain=${plain}, count=${count}`);

  function handlePlain() {
    plain = plain + 1;
    console.log(`handlePlain: plain=${plain}`);
  }

  return (
    <div>
      <button onClick={handlePlain}>plain +1</button>{' '}
      <button onClick={() => setCount(count + 1)}>state +1</button>
      <p>plain: {plain} · state: {count}</p>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Counter />);
</script>

ওই ক্রমে চাপলে সত্যিকারের log আসে:

render: plain=0, count=0
handlePlain: plain=1
handlePlain: plain=2
handlePlain: plain=3
render: plain=0, count=1

দুটি আলাদা ভুল এখানে ধরা পড়ল। প্রথমত, plain তিনবার বেড়েছে, কিন্তু কোনো render: লাইন আসেনি, তাই স্ক্রিন 0-তেই আটকে আছে। দ্বিতীয়ত, state +1 চাপতেই component আবার চলল, আর let plain = 0 লাইনটিও আবার চলল। ফলে 3 মুছে আবার 0. আসলে component একটি সাধারণ function, আর function শেষ হলে তার local variable-ও শেষ।

এখান থেকে দুটি দরকার স্পষ্ট হয়। এক, মানটি এক render থেকে পরের render পর্যন্ত টিকে থাকতে হবে। দুই, মান বদলালে React-কে জানাতে হবে, যাতে সে স্ক্রিন নতুন করে আঁকে। State এই দুটি কাজই করে।

চায়ের দোকানের কথা ভাবুন। কর্মচারী হাতের তালুতে কলম দিয়ে বাকির হিসাব লিখলেন। হাত ধুলেই হিসাব শেষ, আর মালিকও কিছু জানলেন না। এটাই let variable. অন্যদিকে মালিকের ক্যাশবাক্সের পাশে একটি বাকির খাতা আছে। কর্মচারী সেখানে সরাসরি কলম চালাতে পারেন না। তিনি মালিককে বলেন, “রহিম ভাইয়ের বাকি ৫০ করে দিন।” মালিক খাতা ঠিক করে দোকানের বোর্ডটিও নতুন করে লেখেন। React হলো সেই মালিক, খাতা হলো state, আর setCount হলো মালিককে বলা অনুরোধ।

২ · useState — React-এর কাছে রাখা খাতা

useState একটি hook, মানে React-এর ভেতরের সুবিধায় ঢোকার একটি বিশেষ function. এটি ডাকলে দুটি জিনিস পাবেন, একটি দুই ঘরের array হিসেবে:

import { useState } from "react";

function ProductCard() {
  const [qty, setQty] = useState(1);
  // qty: এখনকার মান, setQty: মান বদলানোর অনুরোধ, 1: প্রথমবারের মান
  return <button onClick={() => setQty(qty + 1)}>পরিমাণ: {qty}</button>;
}

এই পর্বে নিচের ব্লকগুলোতে import লেখা হবে না, কারণ পাতার ভেতরের React পরীক্ষাগারে সব export আগে থেকেই আছে। নিজের Vite project-এ import লাগবে, যেমন পর্ব ০৫-এ কাজের পরিবেশ তৈরির সময় দেখেছেন। লেখার সময়ে React-এর সংস্করণ 19.3.1.

ঠিক কী ঘটে, তা তিন ধাপে মনে রাখুন। React-এর নিজের ভাষাতেও এই তিনটি নাম আছে: trigger, render, commit.

একটি ক্লিক থেকে নতুন স্ক্রিন

Trigger → Render → Commit

Event, setState, render আর commit-এর চক্র Event: ব্যবহারকারীর ক্লিক Trigger: setCount(1) Render: component আবার চলে Commit: DOM-এ শুধু বদলটুকু handler চলে React-কে অনুরোধ, এখনই কিছু বদলায় না নতুন মান দিয়ে নতুন JSX তারপর browser স্ক্রিন আঁকে
ছবি ১০.১ — ডান পাশের তীর বলে, স্ক্রিন বদলানোর পর React পরের event-এর জন্য অপেক্ষা করে।

“Render” শব্দটি নিয়ে একটি ভুল ধারণা খুব প্রচলিত। Render মানে স্ক্রিনে আঁকা নয়। Render মানে React আপনার component function-টি ডাকল। ওই function যে JSX ফেরত দিল, তার সাথে আগের JSX মিলিয়ে React দেখে কী বদলেছে। তারপর commit ধাপে শুধু সেটুকুই DOM-এ বসায়। এই মেলানোর ভেতরের নিয়ম আসবে পর্ব ১৫-এ।

useState(1)-এর 1 শুধু প্রথম render-এ কাজে লাগে। পরের render-গুলোতে React খাতা থেকে এখনকার মান ফেরত দেয়। Hook ডাকার নিয়মও এখানেই: component-এর একদম ওপরের স্তরে ডাকতে হয়, if বা loop-এর ভেতরে নয়। কেন, তা প্র ০৪-এ দেখবেন। পুরো নিয়ম আসবে পর্ব ১৪-এ।

৩ · State একটি snapshot

এই অংশটি পরিষ্কার হলে পুরো পর্ব সহজ হয়ে যাবে। আগে একটি ছোট প্রশ্ন। নিচের handler-এ setCount(count + 1)-এর ঠিক পরের লাইনে console.log(count) আছে। ক্লিকে কী ছাপা হবে, 0 নাকি 1? অনেকে 1 বলেন, আর তখনই হোঁচট খান।

React-এর উত্তর 0. কারণ count কোনো জীবন্ত বাক্স নয়। এটি একটি সাধারণ const, যার মান ওই একটি render-এর জন্য বাঁধা। Render যেন ক্যামেরার একটি ছবি, আর সেই ছবির ভেতরে থাকে props, state আর event handler, সবই ওই মুহূর্তের। Handler নিজের ছবির count মনে রাখে, যা JavaScript-এর closure-এর সাধারণ আচরণ। “পরে” চেপে তিনটি render ধরে মানগুলো দেখুন।

1function Counter() {2  const [count, setCount] = useState(0);3  function handleClick() {4    setCount(count + 1);5    console.log(count);6  }7  return <button onClick={handleClick}>{count}</button>;8}
এই মুহূর্তের মান
কোন render
count (ছবির ভেতরে)
React-এর খাতা
স্ক্রিনে
  1. Render ১ শুরু। React প্রথমবার Counter ডাকল। খাতা খালি, তাই useState(0) দিল 0. এই render-এর ছবিতে count এখন চিরকাল 0.
  2. Render ১ শেষ, commit হলো। return থেকে পাওয়া JSX DOM-এ বসল, স্ক্রিনে 0. বোতামে যে handleClick লাগল, সে render ১-এর ছবির count মনে রাখে।
  3. প্রথম ক্লিক। Handler চলল। setCount(0 + 1) মানে “পরের render-এ 1 দিন”, এই অনুরোধ জমা পড়ল। count ভেরিয়েবল নিজে বদলায়নি।
  4. পরের লাইন। console.log(count) ছাপল 0, কারণ handler এখনো render ১-এর ছবির ভেতরে। স্ক্রিনও এখনো 0.
  5. Render ২ শুরু। Handler শেষ হতেই React আবার Counter ডাকল। এবার useState খাতা থেকে দিল 1. একই লাইন, নতুন function call, নতুন count.
  6. Render ২ commit. স্ক্রিনে 1. React বোতামের সাথে নতুন handleClick জুড়ে দিল, যার ছবিতে count = 1. আগের handler আর ব্যবহার হবে না।
  7. দ্বিতীয় ক্লিক। নতুন handler চলল: setCount(1 + 1), আর log ছাপল 1, অর্থাৎ আবারও “পুরোনো” মান। নিয়ম একই, তাই আচরণও একই।
  8. Render ৩। খাতা থেকে এলো 2, স্ক্রিনে 2. দুই ক্লিকে log হলো 0, 1, আর স্ক্রিন হলো 1, 2. Log সবসময় স্ক্রিনের সাথে মেলে, ক্লিকের আগের স্ক্রিনের সাথে।

এই আচরণ বাগ নয়, বরং একটি নিরাপত্তা। ধরুন বাজারঘরে একটি বোতাম ৩ সেকেন্ড পরে অর্ডার পাঠায়:

function SendOrder() {
  const [phone, setPhone] = useState("০১৭১১");

  function handleSend() {
    setTimeout(() => console.log("অর্ডার যাচ্ছে:", phone), 3000);
  }
  // ...
}

ক্লিকের পর ৩ সেকেন্ডের মধ্যে ব্যবহারকারী ফোন নম্বর বদলালেও phone থাকবে ক্লিকের মুহূর্তের মান। আপনি যে স্ক্রিন দেখে চেপেছিলেন, অর্ডারও সেই স্ক্রিনের মান নিয়েই যায়। তবে কখনো কখনো ঠিক উল্টোটা দরকার হয়, মানে সর্বশেষ মান। তখন এই পুরোনো মানকে বলে stale closure, যার সমাধান আসবে পর্ব ১৩ আর ১৪-এ।

মূল ধারণা

একটি render-এর ভেতরে state কখনো বদলায় না। setX শুধু পরের render-এর জন্য অনুরোধ জমা দেয়। নতুন মান দেখা যায় কেবল পরের render-এ।

৪ · Batching আর updater function

রেস্তোরাঁর ওয়েটার তিনজনের টেবিলে গিয়ে প্রথম পদ শুনেই রান্নাঘরে দৌড়ান না। তিনি পুরো অর্ডার খাতায় টুকে নেন, তারপর একবারে যান। React-ও তাই করে। একটি handler-এ যতগুলো set ডাকা হোক, সবগুলো একটি সারিতে জমা হয়। Handler শেষ হলে React সারিটি শেষ পর্যন্ত হিসাব করে একটিমাত্র render করে। একেই বলে batching.

এবার ক্লাসিক প্রশ্নটি। এক ক্লিকে তিনবার setCount(count + 1) লিখলে কত বাড়বে? নিচে দুটি বোতাম। প্রথমটি মান পাঠায়, দ্বিতীয়টি updater function পাঠায়। দুটিই একবার করে চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Triple() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  console.log(`render: a=${a}, b=${b}`);

  function byValue() {
    setA(a + 1);
    setA(a + 1);
    setA(a + 1);
    console.log(`byValue শেষ, ছবির a=${a}`);
  }

  function byUpdater() {
    setB(x => x + 1);
    setB(x => x + 1);
    setB(x => x + 1);
    console.log(`byUpdater শেষ, ছবির b=${b}`);
  }

  return <div>
    <button onClick={byValue}>মান: {a}</button>{' '}
    <button onClick={byUpdater}>updater: {b}</button>
  </div>;
}
createRoot(document.getElementById('root')).render(<Triple />);
</script>

দুটি বোতাম একবার করে চাপলে সত্যিকারের log:

render: a=0, b=0
byValue শেষ, ছবির a=0
render: a=1, b=0
byUpdater শেষ, ছবির b=0
render: a=1, b=3

প্রথম বোতামে তিনটি লাইনই আসলে setA(0 + 1), কারণ ছবিতে a = 0. তিনবার “1 বসান” বলা মানে ফল 1. দ্বিতীয় বোতামে x => x + 1 কোনো মান নয়, একটি নির্দেশ: “সারিতে আগের ফল যা-ই হোক, তার সাথে 1 যোগ করুন”। তাই 0 → 1 → 2 → 3. দুই ক্ষেত্রেই এক ক্লিকে ঠিক একটি render: লাইন এসেছে, এটাই batching.

সারির নিয়ম খুব ছোট। মান পাঠালে মানে “আগের সব ফেলে এটি বসান”। Function পাঠালে মানে “আগের ফল নিয়ে এই হিসাব করুন”। মিশিয়ে দিলে কী হয়, ধাপে ধাপে দেখুন।

1// এখন n = 02function handleClick() {3  setN(n + 5);4  setN(x => x + 1);5  setN(x => x * 2);6}
Handler আর React-এর সারি
ছবির n
সারি (queue)
চলমান ফল
মোট render
স্ক্রিনে
  1. ক্লিক। Handler শুরু। ছবিতে n = 0, সারি খালি, প্রথম render-টি আগেই হয়ে গেছে।
  2. setN(n + 5) মানে setN(5). সারিতে জমা পড়ল “replace 5”. কিছুই চলেনি।
  3. setN(x => x + 1) সারিতে function হিসেবে জমা পড়ল। এটিও এখনো চলেনি, শুধু লেখা হলো।
  4. setN(x => x * 2) জমা পড়ল। Handler শেষ। লক্ষ করুন, তিনটি set-এর পরও মোট render ১টিই।
  5. Render শুরু, সারির প্রথম আইটেম। React পরের render-এ useState-এর কাছে এসে সারি হিসাব করে। “replace 5”, ফল 5.
  6. দ্বিতীয় আইটেম। x => x + 1 পেল x = 5, ফল 6. Updater function render-এর সময়েই চলে, handler-এর সময়ে নয়।
  7. তৃতীয় আইটেম। x => x * 2 পেল x = 6, ফল 12. সারি খালি।
  8. Commit. useState ফেরত দিল 12. ওই render-এর ছবিতে n = 12, স্ক্রিনে 12. তিনটি আপডেটের জন্য render হলো মাত্র একটি।

সারির ফল হিসাব করার নিয়মটি হাতে করা সহজ, পরীক্ষায়ও এটি প্রায়ই আসে। ধরুন n = 0, আর handler-এ আছে setN(n + 5); setN(x => x + 1); setN(x => x * 2); setN(n + 1);

সারির আইটেমx (আগের ফল)ফল
setN(0 + 5)0, অব্যবহৃত5
x => x + 156
x => x * 2612
setN(0 + 1)12, অব্যবহৃত1

শেষের মানটি আগের সব কাজ মুছে দিল। ফল 1, render একটি। React 19.3.1-এ চালিয়ে আমরা এই একই ফল পেয়েছি। কয়েকটি আরও মাপ, একটি local run থেকে:

যা করা হলোবাড়তি render
একসাথে তিনটি আলাদা state বদল১
setTimeout-এর ভেতরে দুটি state বদল১
একটি await-এর আগে একটি, পরে একটি state বদল২

শেষ সারিটি মনে রাখবেন। await-এ handler থেমে যায়, তাই React আগের আপডেটটুকু নিয়ে render করে ফেলে। আর দুটি আলাদা ক্লিক কখনো এক batch-এ যায় না, প্রতিটি ক্লিক নিজের মতো সম্পূর্ণ হয়।

কখন কোনটি? নতুন মান যদি আগের মানের ওপর নির্ভর করে, তাহলে updater লিখুন: setCount(c => c + 1). আগের মানের সাথে সম্পর্ক না থাকলে সরাসরি মান দিন: setOpen(false). Updater function-এর ভেতরে শুধু হিসাব করে ফেরত দিন, API call বা অন্য set নয়। কারণ Strict Mode-এ development-এর সময় React এটি দুবার চালায়, যাতে অশুদ্ধ কোড ধরা পড়ে। ওই Strict Mode-এই আপনার component-এর log-ও দুবার দেখা যায়, যা production-এ হয় না।

৫ · Event handler — function পাঠান, ডাকবেন না

DOM event-এর মূল ধারণা, মানে capture আর bubble, JavaScript কোর্সের event পর্বে আছে। এখানে শুধু React-এর দিকটি দেখি। Event handler হলো সেই function, যা কোনো ঘটনা ঘটলে চলে। JSX-এ এটি camelCase prop হিসেবে যায়, যেমন onClick, onChange, onSubmit.

নতুনদের সবচেয়ে সাধারণ ভুল এখানে। পার্থক্য শুধু দুটি ব্র্যাকেটের:

লেখাকী হয়
onClick={handleClick}ঠিক। Function পাঠানো হলো, ক্লিকে চলবে
onClick={() => addToCart(5)}ঠিক। নতুন একটি function পাঠানো হলো, ক্লিকে সে addToCart(5) ডাকবে
onClick={handleClick()}ভুল। Render-এর সময়েই চলে যাবে, ক্লিক ছাড়াই
onClick={addToCart(5)}ভুল। ভেতরে setState থাকলে render থেকে আবার render, শেষে error

শেষ সারির লুপটি মনে রাখার মতো। Render চলছে, সেখান থেকে setState, তাতে আবার render. React কিছুক্ষণ পর থামিয়ে দেয়, আর আপনি দেখেন Too many re-renders. এই মেসেজ দেখলেই প্রথমে খুঁজুন, কোথাও onClick={someFunction()} আছে কি না।

Handler যে e পায়, তা ব্রাউজারের আসল event নয়, React-এর মোড়ানো একটি object, যার নাম synthetic event. এর আচরণ সব browser-এ একই রকম, আর আসল event চাইলে e.nativeEvent-এ পাবেন। React প্রতিটি বোতামে আলাদা listener বসায় না, root-এ একটি জায়গায় বসায়।

দুটি method সবচেয়ে কাজের, আর এই দুটি একদম আলাদা জিনিস:

e.preventDefault()

Browser-এর নিজের কাজ থামায়। যেমন form জমা দিলে পাতা reload হওয়া, বা link-এ চাপলে অন্য পাতায় যাওয়া।

Browser-কে থামানো

e.stopPropagation()

Event-কে parent-এর handler পর্যন্ত যেতে দেয় না। অর্থাৎ bubbling থামায়। Browser-এর নিজের কাজ এতে থামে না।

Parent-কে থামানো

বাজারঘরের product card-এ দুটিই দরকার হয়। পুরো card-এ ক্লিক করলে পণ্যের পাতা খোলে, আর ভেতরের “কার্টে যোগ করুন” বোতামে ক্লিক করলে শুধু কার্টে যায়:

function ProductCard({ product, onOpen, onAdd }) {
  function handleAdd(e) {
    e.stopPropagation();          // card-এর onClick যেন না চলে
    onAdd(product.id);
  }

  function handleSearch(e) {
    e.preventDefault();           // পাতা reload হবে না
    console.log("খোঁজা হচ্ছে");
  }

  return (
    <div onClick={() => onOpen(product.id)}>
      <h3>{product.name}</h3>
      <button onClick={handleAdd}>কার্টে যোগ করুন</button>
      <form onSubmit={handleSearch}><button>খুঁজুন</button></form>
    </div>
  );
}

এখানে onAdd আর onOpen নামগুলো খেয়াল করুন। নিজের component-এর prop-এ on…, আর ভেতরের function-এ handle…, এটাই React-এর প্রচলিত রীতি। Form-এর পুরো কাজ আসবে পর্ব ১২-এ। React-এ onScroll ছাড়া সব event bubble করে, আর onClickCapture লিখলে নিচে নামার পথেই event ধরা যায়, যা সাধারণ app কোডে কম লাগে।

৬ · Object আর array — state না ভেঙে আপডেট

এতক্ষণ state ছিল একটি সংখ্যা। কার্ট কিন্তু object-এর array. এখানেই সবচেয়ে কঠিন বাগটি লুকিয়ে থাকে। নিচের ব্লকে দুটি বোতাম আছে। “ভুল” বোতাম দুবার চাপুন, তারপর “ঠিক” বোতাম একবার।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Bug() {
  const [items, setItems] = useState(['চাল']);
  console.log(`render: ${items.join(', ')}`);

  function wrong() {
    items.push('ডাল');
    setItems(items);          // একই array
    console.log(`wrong: ভেতরে এখন ${items.length}টি`);
  }

  function right() {
    setItems([...items, 'তেল']);   // নতুন array
  }

  return <div>
    <button onClick={wrong}>ভুল: push</button>{' '}
    <button onClick={right}>ঠিক: নতুন array</button>
    <p>কার্টে {items.length}টি: {items.join(', ')}</p>
  </div>;
}
createRoot(document.getElementById('root')).render(<Bug />);
</script>

ওই ক্রমে চাপলে সত্যিকারের log:

render: চাল
wrong: ভেতরে এখন 2টি
wrong: ভেতরে এখন 3টি
render: চাল, ডাল, ডাল, তেল

“ভুল” বোতামে array সত্যিই বড় হয়েছে, কিন্তু স্ক্রিন “চাল”-এই ছিল, আর কোনো render: লাইনও আসেনি। কারণ setItems পেল ঠিক সেই পুরোনো array-টিই। React Object.is দিয়ে পুরোনো আর নতুন মান মেলায়। একই reference মানে “কিছু বদলায়নি”, তাই কাজ শেষ। সবচেয়ে বিপজ্জনক অংশটি শেষ লাইনে: “ঠিক” চাপতেই লুকানো দুটি “ডাল” হঠাৎ ভেসে উঠল। বাস্তবে এই বাগ এভাবেই আসে, ভুল এক জায়গায়, আর লক্ষণ দেখা যায় অন্য কোনো ক্লিকে।

একটি সূক্ষ্ম কথা। একই মান পাঠালে React সবসময় পুরোপুরি থামে না। কোনো আপডেটের ঠিক পরে এমন হলে সে component function আরেকবার ডাকতে পারে, তারপর ফল বাদ দিয়ে থেমে যায়। আমাদের local run-এও এটি দেখা গেছে: সদ্য একটি আপডেটের পর একই মান পাঠালে render গোনা হলো ১টি। তবে DOM তাতে বদলায় না, আর মিউটেশনের বাগও তাতে সারে না।

তাই নিয়ম একটাই: state-কে শুধু পড়ার জিনিস ভাবুন। একে বলে immutable update, মানে পুরোনো জিনিস না বদলে নতুন কপি বানানো। আর সরাসরি বদলে ফেলাকে বলে mutation. পুরোনো ছবি মুছে পাল্টানোর মতো, এতে snapshot-এর পুরো নিশ্চয়তাই ভেঙে যায়।

কাজএড়িয়ে চলুনলিখুন
যোগpush, unshift[...items, x]
বাদpop, spliceitems.filter(i => i.id !== id)
একটি আইটেম বদলitems[0].qty = 2items.map(i => i.id === id ? { ...i, qty: 2 } : i)
সাজানোsort, reverseitems.toSorted(fn)
Object-এ ঘর বদলuser.name = "রিনা"{ ...user, name: "রিনা" }

... চিহ্নটি spread syntax, বিস্তারিত JavaScript কোর্সের destructuring আর spread পর্বে আছে। একটি সতর্কতা: spread শুধু এক স্তর কপি করে। { ...user }-এর ভেতরের address object আগেরটিই থেকে যায়। ভেতরের কিছু বদলাতে হলে সেই স্তরটিও কপি করতে হয়।

এবার সব মিলিয়ে বাজারঘরের সত্যিকারের কার্ট। এই ক্রমে চাপুন: ডাল যোগ, চালের পাশে +, আবার ডাল যোগ, তারপর চালের পাশে বাদ.

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const dal = { id: 2, name: 'ডাল', price: 120, qty: 1 };
function Cart() {
  const [items, setItems] = useState([{ id: 1, name: 'চাল', price: 80, qty: 1 }]);
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  console.log(`লাইন ${items.length}টি, মোট ${total} টাকা`);

  const add = () => setItems(prev => prev.some(i => i.id === dal.id)
    ? prev.map(i => i.id === dal.id ? { ...i, qty: i.qty + 1 } : i)
    : [...prev, dal]);
  const changeQty = (id, by) => setItems(prev => prev.map(i =>
    i.id === id ? { ...i, qty: Math.max(1, i.qty + by) } : i));
  const remove = id => setItems(prev => prev.filter(i => i.id !== id));

  return (
    <div>
      {items.map(i => (
        <p key={i.id}>
          {i.name} × {i.qty}{' '}
          <button onClick={() => changeQty(i.id, 1)}>+</button>
          <button onClick={() => changeQty(i.id, -1)}>−</button>
          <button onClick={() => remove(i.id)}>বাদ</button>
        </p>))}
      <button onClick={add}>ডাল যোগ</button> <b>মোট: {total} টাকা</b>
    </div>);
}
createRoot(document.getElementById('root')).render(<Cart />);
</script>

ডাল যোগ, চালে +, ডাল যোগ, চাল বাদ, এই ক্রমে সত্যিকারের log:

লাইন 1টি, মোট 80 টাকা
লাইন 2টি, মোট 200 টাকা
লাইন 2টি, মোট 280 টাকা
লাইন 2টি, মোট 400 টাকা
লাইন 1টি, মোট 240 টাকা

হাতে মিলিয়ে নিন। চাল ৮০ × ২ + ডাল ১২০ × ১ = ২৮০. আবার ডাল যোগে ৮০ × ২ + ১২০ × ২ = ৪০০. চাল বাদে ১২০ × ২ = ২৪০. তিনটি কাজের প্রতিটি নতুন array বানিয়েছে, কোথাও push নেই। total কোনো state নয়, প্রতিটি render-এ হিসাব হয়। key নিয়ে বিস্তারিত আসবে পর্ব ১১-এ।

“হাজার আইটেমের কার্টে প্রতিবার কপি করলে ধীর হবে না?” map নতুন array বানায় ঠিকই, কিন্তু object-গুলো কপি করে না, শুধু reference নেয়। আমরা ১০০০ আইটেমের array-তে ৫০০ নম্বর আইটেম বদলে মেপেছি: ৯৯৯টি object হুবহু আগের, নতুন মাত্র ১টি। একে বলে structural sharing. খরচ ১০০০ ঘরের একটি array, অর্থাৎ O(n), যা সাধারণ কার্টে নগণ্য।

Linter কি এই ভুল ধরে? আংশিক। নিচের ফলটি সত্যিকারের run, eslint-plugin-react-hooks 7.1.1 আর ESLint 10.14.0 দিয়ে। src/QtyBug.jsx-এ item.qty = item.qty + 1 আছে, আর src/PushBug.jsx-এ উপরের মতো items.push(...).

সত্যিকারের run · project-এর পথ বদলে ~/bazarghar করা হয়েছে

$ npx eslint src
~/bazarghar/src/QtyBug.jsx
  7:5  error  Error: This value cannot be modified

Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead.

>  7 |     item.qty = item.qty + 1;
     |     ^^^^ `item` cannot be modified
  …  react-hooks/immutability

✖ 1 problem (1 error, 0 warnings)

ঘরে মান বসানো ধরা পড়েছে, কিন্তু PushBug.jsx নিয়ে একটি শব্দও আসেনি। এই সংস্করণের এই configuration-এ push পার হয়ে গেছে। তাই linter সবুজ মানেই কোড ঠিক নয়। নিয়মটি আপনার নিজের মাথায় থাকতে হবে। ভবিষ্যতের সংস্করণে এই আচরণ বদলাতে পারে।

৭ · State-এর গঠন — যা হিসাব করা যায়, তা রাখবেন না

কোন জিনিস state হবে, এই সিদ্ধান্ত বেশিরভাগ বাগ শুরু হওয়ার আগেই থামাতে পারে। চায়ের দোকানের খাতায় প্রতিটি কাপের দাম লেখা থাকে। মোট টাকা আলাদা করে আরেক খাতায় লিখলে কী হবে? একদিন একটি খাতা ঠিক হবে, অন্যটি ভুলে যাবেন। তখন দুই খাতার ঝগড়া। React-এর state-এও ঠিক একই ঘটনা ঘটে।

নিচের কোডে দুটি সমস্যা আছে। প্রথমে নিজে খুঁজে দেখুন:

function Cart({ initialItems }) {
  const [items, setItems] = useState(initialItems);
  const [total, setTotal] = useState(0);           // redundant
  const [selected, setSelected] = useState(items[0]);  // duplicate

  function addOne(id) {
    setItems(/* ... */);
    setTotal(/* আবার হিসাব, ভুলে গেলেই বাগ */);
  }
}
function Cart({ initialItems }) {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(initialItems[0]?.id);

  // হিসাব করা মান: state নয়, render-এর সময় বের হয়
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  const selected = items.find(i => i.id === selectedId);
}

total ছিল redundant state, কারণ items থেকেই বের করা যায়। এমন মানকে বলে derived state. আর selected ছিল duplicate state, কারণ একই object দুই জায়গায় ছিল। items-এ পরিমাণ বদলালে selected-এর কপি পুরোনোই থেকে যেত। সমাধান, শুধু id রাখা। React-এর নিজের নথিতে এই পাঁচটি নিয়ম আছে:

নিয়মখারাপ উদাহরণভালো উদাহরণ
সম্পর্কিত state এক করাx আর y আলাদা, সবসময় একসাথে বদলায়{ x, y } একটি state
পরস্পরবিরোধী state এড়ানোisSending আর isSent দুটিই true হতে পারেstatus: "sending" বা "sent"
Redundant state এড়ানোtotal staterender-এ হিসাব
Duplicate state এড়ানোselected পুরো objectselectedId
গভীর nesting এড়ানোবিভাগের ভেতরে বিভাগের ভেতরে পণ্যid দিয়ে সমতল গঠন

একটি বিশেষ ভুল আলাদা করে বলা দরকার: prop-কে state-এ কপি করা। useState(messageColor) লিখলে parent পরে নতুন রং পাঠালেও state আগেরটিই থাকবে, কারণ useState-এর argument শুধু প্রথম render-এ কাজ করে। যদি সত্যিই শুধু শুরুর মান চান, নামেই তা বলে দিন: initialItems, যেমন উপরের কোডে। এই নিয়মে ডেটাবেসের normalization-এর গন্ধ পাচ্ছেন? ঠিকই, চিন্তাটা একই: একটি তথ্য এক জায়গায়, বাকিটা সেখান থেকে হিসাব।

৮ · State ওপরে তোলা — দুই ভাইবোনের ভাগের তথ্য

বাজারঘরের পাতায় দুটি component পাশাপাশি: ProductCard আর ওপরের CartBadge, যেখানে কার্টের সংখ্যা দেখায়। Card-এ চাপ দিলে Badge কীভাবে জানবে? React-এ ডেটা শুধু ওপর থেকে নিচে যায়, তাই পাশাপাশি দুটি component সরাসরি কথা বলতে পারে না। এর উত্তর lifting state up: state-কে দুজনের সবচেয়ে কাছের সাধারণ parent-এ তুলে নেওয়া।

আগেstate নিচে, Badge কিছুই জানে না
Shop
├─ ProductCard
│    count: 1 (নিজের)
└─ CartBadge
     count: 0 (নিজের)
ধাপ ১state উঠল parent-এ
Shop
│    cartCount: 0
├─ ProductCard
│    state নেই
└─ CartBadge
     state নেই
ধাপ ২নিচে মান আর function
Shop
│    cartCount: 0
├─ ProductCard
│    onAdd={handleAdd}
└─ CartBadge
     count={0}
ক্লিকঘটনা ওপরে উঠছে
Shop
│    cartCount: 0
├─ ProductCard
│    onAdd() ডাকল ↑
└─ CartBadge
     count={0}
Renderparent আপডেট
Shop
│    cartCount: 1
├─ ProductCard
│    onAdd={handleAdd}
└─ CartBadge
     count={0}
ফলদুই সন্তানই নতুন render
Shop
│    cartCount: 1
├─ ProductCard
│    onAdd={handleAdd}
└─ CartBadge
     count={1} → স্ক্রিনে 1
  1. সমস্যা। দুজনের নিজের নিজের state. Card-এ ক্লিকে তার count হলো 1, কিন্তু Badge-এর 0-ই রইল। একই তথ্য দুই জায়গায়, আর দুটি একমত নয়।
  2. State ওপরে তোলা। দুজনের কাছের সাধারণ parent হলো Shop. cartCount এখন শুধু সেখানে, মান 0. নিচের দুটি থেকে state মুছে দেওয়া হলো।
  3. Props দিয়ে নিচে পাঠানো। Badge পেল মান, count={0}. Card পেল একটি function, onAdd. ডেটা নিচে যায়, আর ঘটনা function দিয়ে ওপরে আসে।
  4. ক্লিক। ব্যবহারকারী Card-এ চাপলেন। Card নিজে কিছুই বদলাতে পারে না, সে শুধু onAdd() ডাকল। Parent-এর দেওয়া function, তাই কাজটি হবে parent-এর ভেতরে।
  5. Parent-এর state বদল। setCartCount(c => c + 1) চলল, cartCount 0 থেকে হলো 1. Shop re-render হলো। এখনো Badge আগের props-এই আছে।
  6. সন্তানেরা নতুন props পেল। Parent render হলে ডিফল্টভাবে সন্তানরাও render হয়। Badge পেল count={1}, স্ক্রিনে 1. তথ্য একটাই, তাই ঝগড়ার সুযোগ নেই।
function CartBadge({ count }) {
  return <span>কার্টে {count}টি</span>;
}

function ProductCard({ name, onAdd }) {
  return <button onClick={onAdd}>{name} যোগ করুন</button>;
}

function Shop() {
  const [cartCount, setCartCount] = useState(0);
  const handleAdd = () => setCartCount(c => c + 1);

  return (
    <>
      <CartBadge count={cartCount} />
      <ProductCard name="চাল" onAdd={handleAdd} />
    </>
  );
}

এখানে Shop হলো cartCount-এর single source of truth, মানে এই তথ্যের একমাত্র আসল ঠিকানা। State কোথায় রাখবেন, তা ঠিক করার পদ্ধতিটি সহজ: কে কে এই state ব্যবহার করে তা খুঁজুন, তাদের কাছের সাধারণ parent বের করুন, সেখানে রাখুন। অনেক স্তর পার করে props পাঠাতে হলে কী করবেন, আর global store কখন লাগে, তা আসবে পর্ব ১৬ থেকে ১৮-এ।

৯ · State কখন মুছে যায় — position আর key

State component-এর ভেতরে থাকে না, এটা শুনে অবাক লাগতে পারে। State থাকে React-এর কাছে, আর React তা বাঁধে tree-র একটি position-এর সাথে। ডাকঘরের বাক্সের মতো ভাবুন। চিঠি থাকে “১২ নম্বর বাক্সে”, মালিকের পকেটে নয়। বাক্সের গায়ে নাম বদলালেও চিঠি থেকে যায়, আর বাক্সটি সরিয়ে ফেললে চিঠিও শেষ।

তিন ক্ষেত্রে state-এর কী হয়

Counter-এ count = 5 আছে, এবার পরের render

Position, key আর component type-এর প্রভাব ক. একই type, একই position খ. key বদলাল গ. অন্য type বসল <Counter who="A"/> count: 5 <Counter who="B"/> count: 5, রয়ে গেল <Counter key="A"/> count: 5 <Counter key="B"/> count: 0, নতুন <Counter/> count: 5 <p>…</p> state ধ্বংস
ছবি ১০.২ — তিনটি ঘটনাই React 19.3.1-এ চালিয়ে মেলানো হয়েছে।

নিয়মগুলো সংক্ষেপে: একই component একই position-এ থাকলে state থাকে, props বদলালেও। অন্য component বসলে, বা component সরে গেলে, state মুছে যায়। Parent-এর type বদলালেও, যেমন <div> থেকে <section>, ভেতরের সব state মুছে যায়। আর এখানে “position” মানে JSX ফাইলের লাইন নয়, মানে তৈরি হওয়া tree-র জায়গা।

এর একটি ব্যবহারিক ফাঁদ আছে। বাজারঘরের অ্যাডমিন চ্যাটে গ্রাহক বদলালেন, কিন্তু লেখার ঘরে আগের গ্রাহকের খসড়া রয়ে গেল, কারণ ChatBox একই position-এ। সমাধান key:

// গ্রাহক বদলালে key বদলায়, তাই ChatBox নতুন করে তৈরি হয়, খসড়া মুছে যায়
<ChatBox key={customer.id} customer={customer} />

Component-এর ভেতরে component বানাবেন না। function Parent() { function Child() {…} … } লিখলে প্রতিটি render-এ Child নতুন একটি function হয়, মানে React-এর চোখে অন্য type. ফলে প্রতিটি render-এ ভেতরের state মুছে যায়, input থেকে focus-ও হারিয়ে যায়। Component সবসময় ফাইলের ওপরের স্তরে লিখুন।

উন্নত নোট: flushSync আর startTransition

এই দুটি টুল সাধারণ কাজে লাগে না। তবে batching-এর ছবি পূর্ণ করতে চিনে রাখুন। flushSync বলে “অপেক্ষা নয়, এখনই DOM আপডেট করুন”। নিচে React 19.3.1-কে jsdom-এ চালিয়ে দেখা হয়েছে, setN-এর পরের লাইনেই DOM-এ কী আছে।

setN(1);
console.log("no flush DOM:", text(), "renders:", renders);
await new Promise(r => setTimeout(r, 20));
console.log("later   DOM:", text(), "renders:", renders);
flushSync(() => setN(2));
console.log("flushSync DOM:", text(), "renders:", renders);
flushSync(() => setN(3));
flushSync(() => setN(4));
console.log("two flushSync DOM:", text(), "renders:", renders);

সত্যিকারের run · react 19.3.1, react-dom 19.3.1, jsdom 30.1.3, Node 24

$ node flush.mjs
start   DOM: 0 renders: 1
no flush DOM: 0 renders: 1
later   DOM: 1 renders: 2
flushSync DOM: 2 renders: 3
two flushSync DOM: 4 renders: 5

সাধারণ setN(1)-এর পরের লাইনে DOM তখনো 0. flushSync-এর পরের লাইনেই DOM নতুন মানে, আর প্রতিটি flushSync একটি করে বাড়তি render ঘটিয়েছে। অর্থাৎ batching-এর সুবিধা আপনি নিজে হাতে ছেড়ে দিচ্ছেন। React-এর নথি একে শেষ উপায় বলে, কারণ এতে performance খারাপ হতে পারে। এর বৈধ ব্যবহার সাধারণত React-এর বাইরের কোডের সাথে কাজে, যেমন ব্রাউজারের beforeprint event, যেখানে function শেষ হওয়ার আগেই DOM ঠিক থাকা দরকার। আমদানি হয় react-dom থেকে।

startTransition উল্টো কাজ করে: বলে “এই আপডেট জরুরি নয়”। বাজারঘরের ২০,০০০ পণ্যের তালিকা ফিল্টার করতে ভারী render লাগতে পারে। তখন startTransition(() => setTab(next)) লিখলে সেই render চলার মধ্যেও পাতা সাড়া দেয়, আর নতুন আপডেট এলে React ভারী কাজটি থামিয়ে আবার শুরু করে। সীমা তিনটি: টাইপ করার input নিয়ন্ত্রণে এটি চলে না, setTimeout-এর ভেতরের আপডেট transition হয় না, আর await-এর পরের আপডেটে আবার startTransition লিখতে হয়। Pending অবস্থা দেখাতে চাইলে useTransition. Performance নিয়ে বিস্তারিত পর্ব ২৩-এ।

আরও গভীরে: React প্রতিটি component-এর জন্য ভেতরে একটি Fiber node রাখে, আর সেখানে hook-গুলো ডাকার ক্রমে একটি linked list-এ থাকে। প্রতিটি useState-এর সাথে একটি update queue যুক্ত, যা পরের render-এ শেষ পর্যন্ত হিসাব হয়। মান তুলনার জন্য React Object.is ব্যবহার করে, তাই NaN নিজের সমান ধরা হয়। React 18 (২০২২) থেকে createRoot-এ batching timeout আর promise-এর ভেতরেও হয়, আগে হতো শুধু React event-এ।

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

  • state আর props-এর পার্থক্য? Props বাইরে থেকে আসে, শুধু পড়া যায়। State component নিজে পরিচালনা করে।
  • setCount-এর পরে log-এ পুরোনো মান কেন? State ওই render-এর snapshot, নতুন মান পরের render-এ।
  • তিনবার setCount(count + 1)-এ কত বাড়ে? ১, কারণ তিনটিই একই snapshot থেকে হিসাব। Updater দিলে ৩.
  • Array state-এ push কেন চলে না? Reference একই থাকে, তাই Object.is বদল দেখে না।
  • key বদলালে কী হয়? React পুরোনোটি ধ্বংস করে নতুন component বানায়, state শূন্য থেকে শুরু।

১০ · AI-এর যুগে — চলছে দেখেই বিশ্বাস নয়

AI খুব দ্রুত React component লিখে দেয়, আর সেগুলো সাধারণত চলেও। সমস্যা হলো, state-এর বাগ প্রথম চালানোয় দেখা যায় না। দ্রুত দুবার ক্লিক, ধীর নেটওয়ার্ক বা দ্বিতীয় আপডেটে তা বের হয়। তাই “চলছে” এই পর্বের বিষয়ে কোনো প্রমাণই নয়।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
total বা filteredItems state-এ রেখে useEffect-এ মেলানোপ্রশ্ন করুন, এটি কি বাকি state থেকে হিসাব করা যায়?
items.push বা item.qty++, পরে কপি করে “ঠিক” দেখানোHandler-এ mutating method খুঁজুন, sort সহ
পুরোনো class component বা this.setState নমুনাFunction component আর hook-এ লিখতে বলুন, নিজের সংস্করণের নথি দেখুন
পুরোনো লেখা থেকে e.persist() বসানোReact DOM-এ এর দরকার নেই, event pooling React 17-এ উঠে গেছে
flushSync দিয়ে timing “সারানো”আসল কারণ খুঁজুন, সাধারণত snapshot বোঝার ভুল
price ক্লায়েন্টের state থেকে সরাসরি পেমেন্ট API-তে পাঠানোদাম সবসময় server-এ হিসাব করুন, ক্লায়েন্টের state বিশ্বাসযোগ্য নয়

AI-কে কোড লেখাতে হলে state-এর গঠন আপনিই ঠিক করে দিন, আর পরীক্ষার তালিকা চান:

আমি React 19 আর Vite দিয়ে একটি অনলাইন দোকানের কার্ট বানাচ্ছি। শুধু function component আর hook ব্যবহার করো। State হবে শুধু items: {id, name, price, qty}[]. Total, itemCount আর selected item state-এ রাখবে না, render-এ হিসাব করবে। সব আপডেট immutable হবে; push, splice, sort বা ঘরে সরাসরি মান বসানো চলবে না। আগের মান থেকে হিসাব করলে updater function দেবে। নতুন কোনো package যোগ করবে না, দরকার মনে হলে আগে জিজ্ঞেস করবে। কোডের পরে তালিকা দাও: কোন কোন state আছে আর কেন, দ্রুত দুবার ক্লিকে কী হবে, আর আমি নিজে হাতে কোন তিনটি পরীক্ষা করব।

AI-এর লেখা React কোড হাতে পেলে

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

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

প্র ০১setState সাথে সাথে state বদলে দেয় না কেন? এতে তো শিখতে কষ্ট বাড়ে।

এই প্রশ্ন React-এর দলকে বহুবার করা হয়েছে। ২০১৭ সালের দিকে একটি GitHub আলোচনায় Dan Abramov এর উত্তরে কয়েকটি কারণ লিখেছিলেন, আর মূল কথাগুলো আজও খাটে।

  • ধারাবাহিকতা: সাথে সাথে state বদলালেও props বদলাত না, কারণ parent তখনো render হয়নি। তখন একই মুহূর্তে state নতুন আর props পুরোনো থাকত। Lifting state up করলে কোডের অর্থও বদলে যেত। Render পর্যন্ত অপেক্ষা করলে props, state আর DOM একসাথে এগোয়।
  • অর্ধেক UI না দেখানো: তিনটি state বদলের মাঝখানে স্ক্রিন আঁকা হলে ব্যবহারকারী একটি অসম্পূর্ণ অবস্থা দেখতেন, যেমন “কার্ট খালি” কিন্তু “মোট ২৮০ টাকা”।
  • গতি: Batching বাড়তি render বাঁচায়। একটি handler-এ পাঁচটি বদল মানে পাঁচটি নয়, একটি render.

ইতিহাসটিও শিক্ষণীয়। React 18-এর আগে batching হতো শুধু React-এর নিজের event handler-এ। setTimeout বা promise-এর ভেতরে প্রতিটি set আলাদা render ঘটাত, ফলে একই কোড জায়গাভেদে আলাদা আচরণ করত। ২০২২ সালে React 18-এ createRoot-এর সাথে automatic batching আসে, আর নিয়মটি এক হয়। ঠিক এই কারণেই আজ জোর করে সাথে সাথে আপডেট চাইলে আলাদা করে flushSync লিখতে হয়।

দাম একটাই: শুরুতে অস্বাভাবিক লাগে, আর set-এর পরের লাইনে নতুন মান পাওয়া যায় না। সমাধানও সহজ, নতুন মান আগে একটি const-এ রাখুন, তারপর সেটি ব্যবহার করুন। React নিজের সুবিধা নয়, UI-এর ধারাবাহিকতা বেছে নিয়েছে।

মূল উপলব্ধি: “অপেক্ষা করে একবারে করা” একটি নকশার সিদ্ধান্ত, দুর্বলতা নয়। ডেটাবেসের transaction-ও একই চিন্তা: মাঝখানের অসম্পূর্ণ অবস্থা বাইরে দেখাবেন না।

প্র ০২Vue-তে items.push(x) লিখলেই স্ক্রিন বদলায়, React-এ নয়। React এমন কঠিন পথ বেছে নিল কেন?

দুটি framework দুটি আলাদা দর্শনে দাঁড়িয়ে। Vue 3 JavaScript-এর Proxy দিয়ে আপনার object-কে মুড়ে দেয়, তাই কোনো ঘর বদলালেই সে জানতে পারে। Svelte 5-এর runes-ও এই ধরনের, তাই এই দুটিতে mutation স্বাভাবিক। Solid আর Angular-এর signal আলাদা পথে সূক্ষ্ম স্তরে হিসাব রাখে, কে কোন মান পড়ছে। পর্ব ৬৬-এ এই তুলনা আবার আসবে।

React বেছে নিয়েছে উল্টো পথ: UI হলো state-এর ফলাফল, আর state বদলানো যায় না, শুধু নতুন state দেওয়া যায়। এর পেছনে কয়েকটি সুবিধা আছে।

  • বদল ধরা খুব সস্তা: পুরো object ঘুরে দেখতে হয় না। prev === next মিললেই কিছু বদলায়নি। ২০১৩-১৪ সালের দিকে David Nolen-এর ClojureScript লাইব্রেরি Om দেখিয়েছিল, অপরিবর্তনীয় ডেটা React-এর render-কে কতটা দ্রুত করতে পারে।
  • ইতিহাস রাখা সহজ: পুরোনো state অক্ষত থাকে, তাই undo বা time-travel debugging সহজ।
  • Concurrent rendering নিরাপদ: startTransition-এ React একটি render অর্ধেক করে ফেলে দিতে পারে। আগের snapshot অক্ষত না থাকলে সেটি সম্ভব হতো না।

দামও আছে। কোড লম্বা হয়, গভীর nesting-এ spread কষ্টকর। Immer-এর মতো library সেখানে সাহায্য করে: আপনি mutation-এর মতো লেখেন, ভেতরে নতুন কপি তৈরি হয়। আর signal-ভিত্তিক দর্শনের বড় সুবিধা হলো সূক্ষ্ম আপডেট, যেখানে পুরো component আবার চলে না। JavaScript-এ signal যোগের একটি TC39 প্রস্তাবও আলোচনায় আছে, যদিও তা কখনো মান হবে কি না নিশ্চিত নয়। কোন পথ “সেরা”, এ নিয়ে সমাজে একমত নেই।

মূল উপলব্ধি: React-এর immutability কোনো খেয়াল নয়। এটি সরল তুলনা, ইতিহাস আর concurrency কিনতে কিছুটা লেখার সুবিধা ছেড়ে দেওয়া।

প্র ০৩Snapshot আচরণ কি আশীর্বাদ, নাকি অভিশাপ? Stale closure সমস্যাটি আসলে কী?

দুটিই, আর একই বৈশিষ্ট্য থেকে আসে। Dan Abramov ২০১৯ সালে “How Are Function Components Different from Classes?” প্রবন্ধে দেখিয়েছিলেন, class component-এর this.props একটি পরিবর্তনশীল জায়গা, তাই ক্লিকের ৩ সেকেন্ড পরে this.props.user পড়লে ততক্ষণে অন্য ব্যবহারকারীর নাম চলে আসতে পারত। মানে আপনি রহিমের প্রোফাইলে “অনুসরণ” চাপলেন, আর বার্তা গেল করিমের নামে। Function component-এ closure সেই render-এর মান ধরে রাখে, তাই এই বাগ নিজে থেকেই চলে যায়।

অভিশাপটি দেখা যায় যখন দীর্ঘস্থায়ী কিছু পুরোনো ছবি ধরে রাখে। উদাহরণ:

useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);

Effect একবারই চলে, তাই interval সারাজীবন প্রথম render-এর count = 0 দেখে। প্রতি সেকেন্ডে setCount(1), আর স্ক্রিন 1-এই আটকে থাকে। এটাই stale closure, মানে “বাসি” মান ধরে রাখা ছবি। সবচেয়ে সহজ সমাধান এই পর্বেই আছে: setCount(c => c + 1), যা ছবির মান পড়েই না।

আরও সমাধান পরের পর্বগুলোতে আসবে। Dependency array ঠিক করা আসবে পর্ব ১৩-এ। useRef-এ পরিবর্তনশীল মান রাখা আসবে পর্ব ১৪-এ। React 19.2-এ আসা useEffectEvent effect-এর ভেতর থেকে সর্বশেষ মান পড়ার একটি নির্দিষ্ট পথ দেয়।

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

মূল উপলব্ধি: বাগ snapshot-এ নয়, বাগ হলো কোন মুহূর্তের মান চান তা না ভাবা। কোড লেখার আগে প্রশ্নটি করুন।

প্র ০৪দুটি useState পাশাপাশি থাকলে React কীভাবে জানে কোন মান কার? তাদের তো কোনো নাম দেওয়া হয় না।

লক্ষ করুন, useState(0)-এ কোনো id নেই। const [a, setA]-এর নামটি React দেখতেই পায় না, কারণ এটি শুধু array destructuring. তাহলে উপায়? ডাকার ক্রম।

React প্রতিটি component-এর জন্য hook-গুলোর একটি তালিকা রাখে। Render-এর সময় প্রথম useState তালিকার ১ নম্বর ঘর, দ্বিতীয়টি ২ নম্বর ঘর পায়। পরের render-এও ঠিক একই ক্রমে ডাকলে একই ঘর মেলে। রিসেপশনের খাতায় নাম ছাড়া শুধু ক্রমিক নম্বরে অতিথি লেখার মতো: ১ম আসা অতিথি সবসময় ১ নম্বর ঘর।

এবার বোঝা যায় নিয়মটি কেন আছে। ধরুন প্রথম hook একটি if-এর ভেতরে ছিল, আর পরের render-এ শর্ত মিথ্যা হলো। তখন দ্বিতীয় useState ১ নম্বর ঘর পেয়ে যাবে, আর সব মান এক ঘর করে সরে যাবে। এজন্যই hook loop বা শর্তের ভেতরে ডাকা যায় না, আর rules-of-hooks নামে ESLint নিয়ম এটি পাহারা দেয়।

React দল ২০১৮ সালের React Conf-এ Sophie Alpert আর Dan Abramov-এর উপস্থাপনায় Hooks প্রকাশ্যে আনে, আর ২০১৯ সালের শুরুতে React 16.8-এ তা স্থিতিশীল হয়। তখন বিকল্পও ভাবা হয়েছিল, যেমন প্রতিটি state-এ একটি নাম বা key দেওয়া। Dan Abramov পরে একটি প্রবন্ধে লিখেছিলেন, কেন সেগুলো টেকেনি: দুটি custom hook একই নাম ব্যবহার করলে সংঘর্ষ হতো, আর একই hook দুবার ডাকা যেত না। ক্রমভিত্তিক নকশায় এই সমস্যা নেই। Custom hook নিজের ভেতরে যত ইচ্ছা useState ডাকতে পারে, আর তারা তালিকায় আলাদা ঘর পায়।

দাম হলো একটি অদ্ভুত-দেখতে নিয়ম, যা JavaScript নিজে বাধ্য করে না, linter করে। ভেতরের ব্যবস্থাটি Fiber node-এ রাখা একটি linked list.

মূল উপলব্ধি: State component-এর ভেতরে নয়, React-এর খাতায় থাকে। Tree-র position বলে কোন খাতা, আর hook-এর ক্রম বলে কোন লাইন।

প্র ০৫সব state একদম ওপরে তুলে রাখলে তো সমস্যাই থাকে না। তাহলে কেন নিচে রাখব?

লোভনীয় চিন্তা। একটি বড় object, সব তথ্য এক জায়গায়, কোনো ভাগাভাগির ঝামেলা নেই। ২০১৫ সালে Redux জনপ্রিয় হওয়ার পর অনেক দলই এই পথে গিয়েছিল। পরে তারা তিনটি দাম টের পেয়েছে।

  • Render-এর দাম: Parent-এর state বদলালে ডিফল্টভাবে সব সন্তান render হয়। ধরুন App-এ একটি search box-এর লেখা আছে, আর নিচে ৫০০টি product card. প্রতি অক্ষরে ৫০০টি component-ই আবার চলবে। State যদি শুধু SearchBox-এ থাকত, তাহলে চলত মাত্র একটি। memo বা React Compiler এটি কমাতে পারে, কিন্তু সেরা অপ্টিমাইজেশন হলো কাজটিই না থাকা।
  • বোঝার দাম: Dropdown খোলা কি না, এই তথ্য সারা অ্যাপের জানার দরকার নেই। Global জিনিস যত বাড়ে, “এটি কে বদলাতে পারে?” প্রশ্নের উত্তর তত কঠিন হয়। একটি পুরোনো নীতি এখানে খাটে: যত কম জায়গা থেকে দেখা যায়, তত নিরাপদ।
  • পুনর্ব্যবহারের দাম: নিজের state আছে এমন Accordion যেকোনো পাতায় বসানো যায়। ওপরের state-এ বাঁধা থাকলে সেটি আর স্বাধীন নয়।

আবার একদম নিচে রাখলেও সমস্যা, যা ৮ নম্বর অংশের ProductCard আর CartBadge-এ দেখেছেন। তাই নিয়মটি হলো colocation: state যত নিচে রাখা সম্ভব, রাখুন, কিন্তু যারা ব্যবহার করে তাদের সবার ওপরে। শুরুতে নিচে রাখুন, দরকার হলে ওপরে তুলুন। প্রয়োজন ছাড়া আগে থেকে তুলবেন না।

আর সব “state” এক জাতের নয়। সার্ভারের ডেটা মূলত একটি cache, ক্লায়েন্টের নিজের state নয়, যা পর্ব ১৯-এ আসবে। URL-ও একটি state-এর জায়গা, যা পর্ব ২০-এ আসবে। এই পুরো মানচিত্র আসবে পর্ব ১৬-এ। Redux লাগবে কি না, তা নিয়েও সমাজে মতভেদ আছে, উত্তর নির্ভর করে অ্যাপের আকারের ওপর।

মূল উপলব্ধি: State রাখার সঠিক জায়গা ঠিক করে দেয় কারা তা ব্যবহার করে, কোনো সাধারণ রীতি নয়। “যত নিচে সম্ভব, যত ওপরে দরকার”।

অনুশীলন

আগে উত্তর খাতায় লিখুন, তারপর মিলিয়ে দেখুন।

  1. আউটপুট আগে বলুন: n = 10. একটি handler-এ পরপর লেখা আছে setN(n + 1); setN(n + 1); setN(x => x + 2); console.log(n);. Log-এ কী ছাপা হবে? এক ক্লিকের পরে স্ক্রিনে কত? কয়টি render হবে? আবার ক্লিক করলে স্ক্রিনে কত হবে?

    Log ছাপবে 10, কারণ handler পুরোটাই ছবির ভেতরে। সারি হলো: replace 11, replace 11, x + 2. হিসাব: 11 → 11 → 13. তাই স্ক্রিনে 13, render ১টি। পরের ক্লিকে ছবিতে n = 13: replace 14, replace 14, তারপর 14 + 2 = 16, log ছাপবে 13.

  2. Numerical: একটি কার্টে ২,০০০ লাইন আছে। ৭৫০ নম্বর লাইনের qty বদলাতে items.map(i => i.id === 750 ? { ...i, qty: 3 } : i) চালানো হলো। (ক) কতটি নতুন object তৈরি হলো, array ধরে? (খ) পুরোনো array-র কতটি object নতুন array-তে একই reference-এ আছে? (গ) map-এর callback কতবার চলল? (ঘ) ধরে নিন প্রতিটি লাইনের component render করতে ০.২ মিলিসেকেন্ড লাগে। সবগুলো আবার render হলে মোট কত সময়, আর ১৬ মিলিসেকেন্ডের frame-এর কত গুণ?

    (ক) নতুন array ১টি, নতুন item object ১টি, মোট ২টি। (খ) ২,০০০ − ১ = ১,৯৯৯টি। (গ) ২,০০০ বার, তাই সময়ের দিক থেকে O(n). (ঘ) ২,০০০ × ০.২ = ৪০০ মিলিসেকেন্ড, যা ৪০০ ÷ ১৬ = ২৫ গুণ, অর্থাৎ পাতা স্পষ্টভাবে আটকে যাবে। ০.২ মিলিসেকেন্ড এখানে ধরে নেওয়া সংখ্যা, মাপা নয়। শিক্ষাটি হলো, খরচ array কপিতে নয়, প্রায় সবটাই render-এ। অপরিবর্তিত reference থাকায় memo দিয়ে ১,৯৯৯টি লাইনের render বাদ দেওয়া যায় (পর্ব ২৩)। তখন mutation করলে এই কৌশলই বাগ হয়ে যেত।

  3. নকশা সমালোচনা: বাজারঘরের checkout পাতায় একজন লিখেছেন useState দিয়ে: items, total, itemCount, selectedItem, isLoading, isError, isSuccess, firstName, lastName, fullName. কোনগুলো বাদ দেবেন, কোনগুলো বদলাবেন? শেষে কয়টি state থাকবে?

    বাদ: total, itemCount (items থেকে হিসাব), fullName (দুই নাম থেকে হিসাব)। বদল: selectedItem-এর জায়গায় selectedId, যাতে একই object দুই জায়গায় না থাকে। তিনটি is…-এর জায়গায় একটি status: "idle" | "loading" | "error" | "success", কারণ তিনটি boolean মিলে ৮টি অবস্থা হয়, যার মধ্যে অনেকগুলো অর্থহীন, যেমন একসাথে loading আর success. শেষে ৫টি: items, selectedId, status, firstName, lastName. বাস্তবে items আর status সম্ভবত server state হয়ে যাবে (পর্ব ১৯)।

  4. কমান্ড আর নিজের হাতে যাচাই: নিজের Vite project-এ npm i -D eslint eslint-plugin-react-hooks চালিয়ে react-hooks-এর recommended config যোগ করুন। একটি ফাইলে item.qty++ আর আরেকটিতে items.sort() লিখে npx eslint src চালান। কোনটি ধরা পড়ল? তারপর উপরের cart.html ব্লকে একটি “সব মুছুন” বোতাম যোগ করুন।

    আপনার সংস্করণে ফল কী আসে, সেটিই আসল উত্তর। পর্বে দেখানো সংস্করণে সরাসরি ঘরে মান বসানো ধরা পড়েছিল, push ধরা পড়েনি। sort-এর ফল নিজে দেখে নিন। “সব মুছুন” বোতামের কোড:

    <button onClick={() => setItems([])}>সব মুছুন</button>

    এখানে updater লাগে না, কারণ নতুন মান আগের মানের ওপর নির্ভর করে না। items.length = 0 লিখলে তা হতো mutation, আর স্ক্রিন বদলাত না।

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

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

ক্রমএকটি ক্লিক থেকে স্ক্রিন বদলানো পর্যন্ত ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. ব্যবহারকারী বোতামে ক্লিক করেন
  2. Event handler চলে, আর setCount সারিতে অনুরোধ জমা দেয়
  3. Handler শেষ হয়, React সারি হিসাব করে component আবার ডাকে
  4. React নতুন আর পুরোনো JSX মিলিয়ে বদলটুকু বের করে
  5. Commit: DOM-এ শুধু বদলটুকু বসে
  6. Browser নতুন স্ক্রিন আঁকে

একটি React component-এ const [count, setCount] = useState(0); আছে। একটি handler-এ পরপর লেখা setCount(count + 1); console.log(count);. প্রথম ক্লিকে log-এ কী ছাপা হবে?

  • 1
  • 0
  • undefined
  • একটি error

n = 0 হলে এক ক্লিকে setN(n + 5); setN(x => x + 1); setN(x => x * 2); চলে। পরের render-এ n কত?

  • 5
  • 6
  • 12
  • 2

Array state items-এ items.push(x); setItems(items); লিখলে স্ক্রিন বদলায় না কেন?

  • useState array রাখতে পারে না
  • push একটি async কাজ, তাই দেরি হয়
  • key না দেওয়ার কারণে
  • React একই reference পেয়ে ধরে নেয় state বদলায়নি

React-এ e.preventDefault() আর e.stopPropagation()-এর পার্থক্য কী?

  • প্রথমটি browser-এর নিজের কাজ থামায়, দ্বিতীয়টি parent-এর handler-এ যাওয়া থামায়
  • প্রথমটি parent-এ যাওয়া থামায়, দ্বিতীয়টি browser-এর কাজ থামায়
  • দুটি একই কাজ করে
  • দুটিই re-render থামায়

একটি কার্ট component-এ items state আছে, আর মোট দাম দেখাতে হবে। সবচেয়ে ভালো উপায় কোনটি?

  • total নামে আলাদা state রেখে প্রতিটি handler-এ আপডেট করা
  • total state রেখে useEffect দিয়ে items-এর সাথে মেলানো
  • Render-এর সময় items থেকে হিসাব করা
  • DOM থেকে দাম পড়ে যোগ করা

একই জায়গায় <ChatBox customer={c} /> রেখে গ্রাহক বদলালে আগের খসড়া রয়ে যায়। খসড়া মুছে নতুন করে শুরু করার সহজ উপায় কী?

  • flushSync দিয়ে render করা
  • ChatBox-এ key={c.id} দেওয়া
  • customer prop আরেকবার পাঠানো
  • খসড়া একটি সাধারণ let variable-এ রাখা

শব্দকোষ

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

মনে রাখুন

  1. State টিকে থাকে আর re-render ঘটায়; সাধারণ variable কোনোটিই করে না।
  2. প্রতিটি render একটি snapshot; setX শুধু পরের render-এর অনুরোধ।
  3. এক event-এ একটি render; আগের মান থেকে হিসাবে updater function.
  4. State কখনো সরাসরি বদলাবেন না; নতুন array আর object বানান। Function পাঠান, ডাকবেন না।
  5. যা হিসাব করা যায় তা state নয়; ভাগের state কাছের parent-এ তুলুন; state থাকে position-এ, reset করে key.

পরের পর্বে: বাজারঘরের পণ্য একটি নয়, শত শত। Array থেকে list দেখানো, key কেন লাগে, আর “কার্ট খালি” বা “লোড হচ্ছে”-র মতো শর্তসাপেক্ষ UI কীভাবে লিখবেন।

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

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