এই পর্বে যা শিখবেন
mapদিয়ে list আঁকা, আরkeyঠিক কী চিনিয়ে দেয়- Index key-এর input বাগ নিজের হাতে ঘটানো, আর স্থায়ী id দিয়ে সারানো
- “বাজারঘর”-এর পণ্যে search, category filter আর sort: হিসাব করা মান হিসেবে
&&, ternary আর early return-এর মধ্যে কোনটি কোথায়; empty, loading আর error state-এর অভ্যাস- Grouping, nested list,
keyদিয়ে reset, লম্বা list-এর virtualization আর list-এর accessibility
১ · একটি array, অনেকগুলো সারি: map
“বাজারঘর”-এ পণ্য আছে পাঁচটি, কাল হবে পাঁচশো। হাতে পাঁচশোটি <li> কেউ লিখবে না। আমরা পণ্যকে data হিসেবে রাখি, আর একটি নিয়ম লিখি: “প্রতিটি পণ্যের জন্য একটি সারি।” JavaScript-এ এই কাজের টুল map. এটি একটি array নিয়ে সমান দৈর্ঘ্যের নতুন array ফেরত দেয়। পর্ব ০৮-এ JSX দেখেছেন; JSX element-ও একটি সাধারণ মান, তাই map থেকে সেটি ফেরত দেওয়া যায়।
function ProductList({ products }) {
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}JSX-এর { }-এর ভেতরে element-এর array রাখলে React প্রতিটিকে পরপর সন্তান হিসেবে আঁকে। for লুপ JSX-এর ভেতরে চলে না, কারণ { }-এ শুধু মান দেয় এমন expression বসে, statement নয়। map মান দেয়, তাই এখানে সে-ই স্বাভাবিক পছন্দ।
Data থেকে পর্দা পর্যন্ত
Array → map → keyed elements → DOM
২ · key আসলে কী চিনিয়ে দেয়
পর্ব ১০-এ দেখেছেন, state বদলালে component আবার render হয়, আর React নতুন বর্ণনাকে পুরোনোটির সাথে মেলায়। এই মেলানোর কাজের নাম reconciliation; পর্ব ১৫-এ এর ভেতরটা দেখবেন। এখানে প্রশ্ন একটাই: নতুন list-এর তিন নম্বর সারি কি পুরোনো list-এর তিন নম্বর সারিই, নাকি অন্য কেউ?
স্কুলের রোল নম্বরের কথা ভাবুন। প্রতি বছর সারিতে দাঁড়ানোর জায়গা বদলায়, কিন্তু রোল দেখে শিক্ষক জানেন কে কে। Key হলো সেই রোল নম্বর। আর সারির অবস্থান, মানে “বাঁ দিক থেকে তিন নম্বর”, হলো index. কেউ সারি বদলালে অবস্থান দিয়ে ডাকলে অন্য ছাত্র সাড়া দেবেন, খাতাও ভুল জনের হাতে যাবে।
key তিনটি জিনিস ঠিক করে: কোন সারি থেকে গেল, কোনটি সরল, আর কোনটি নতুন বা বাদ পড়ল। থেকে যাওয়া সারির state, DOM node আর input-এর লেখা বেঁচে থাকে। বাদ পড়া সারির সব মুছে যায়।
key-এর নিয়মগুলো
- ভাইবোনদের মধ্যে অনন্য। একই array-তে দুটি একই key চলে না। দুটি আলাদা list-এ একই key থাকতে বাধা নেই, কারণ key শুধু নিজের parent-এর ভেতরে অর্থ রাখে।
- স্থায়ী। একই জিনিসের key পরের render-এও একই থাকবে। Render-এর সময়ে নতুন করে বানালে key-এর উদ্দেশ্যই নষ্ট।
- সবচেয়ে বাইরের element-এ।
mapযে element ফেরত দেয়, key সেখানে বসে; ভেতরের<li>-তে নয়। - Component-এর হাতে আসে না।
keyprops-এর মধ্যে পৌঁছায় না, এটি শুধু React-এর জন্য ইঙ্গিত। Component-এর id লাগলে আলাদা করে পাঠাতে হয়:<Row key={p.id} id={p.id} />.
Key না দিলে React কী বলে? নিচে Vite dev server-এ চালানো একটি ছোট প্রজেক্টের ব্রাউজার console. সত্যিকারের run, React 19.3.1, Vite 8.3.5 (লেখার সময়ে):
Each child in a list should have a unique "key" prop.
Check the render method of `ProductList`. See https://react.dev/link/warning-keys for more information.
দুটি সারিকে একই key দিলে বার্তা বদলায়:
Encountered two children with the same key, `x`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
দুটি বার্তাই আসে development build থেকে। Production build-এ এগুলো থাকে না। এই পাতার live block-গুলোও production build-এ চলে, তাই এখানে key মুছলে কোনো warning পাবেন না। নিজের প্রজেক্টে দেখতে হবে dev server-এ, অথবা linter-এ। সত্যিকারের run, Oxlint 1.90.0:
$ npx oxlint --react-plugin -D react/jsx-key \
-D react/no-array-index-key src/ProductList.jsx
src/ProductList.jsx:9:17: error react(jsx-key): Missing "key" prop
for element in iterator. …
src/ProductList.jsx:25:35: error react(no-array-index-key): Usage of
Array index in keys is not allowed …এই run-এ একটি কথা মনে রাখার মতো: একই ফাইলের key="x"-এর ভুলটি linter ধরেনি, ধরেছিল শুধু dev console. একটি টুলে পাস করলেই ভুল নেই, এমন নয়।
React ২০১৩ সালে উন্মুক্ত হয়। দুটি গাছের পূর্ণ তুলনার সেরা জানা পদ্ধতিগুলোর খরচ ছিল O(n³), মানে ১,০০০ element-এ প্রায় ১০০ কোটি তুলনা। React-এর পুরোনো নথি বলে, তারা দুটি অনুমানের ওপর O(n) পদ্ধতি নেয়। প্রথমটি: ভিন্ন ধরনের element মানে ভিন্ন গাছ। দ্বিতীয়টি: কোন সন্তান স্থায়ী, সেটি developer key দিয়ে জানিয়ে দেবেন। অর্থাৎ key কোনো অলংকার নয়, পুরো নকশার একটি অংশ।
৩ · Index-কে key করলে কী ভাঙে
Key না দিলে React নিজেই index ব্যবহার করে। key={i} লিখলে warning চলে যায়, কিন্তু আচরণ একই থাকে। মানে warning বন্ধ হলো, বাগ রয়ে গেল। নিচের ব্লকে একই তিনটি পণ্য দুই রকম key দিয়ে আঁকা আছে। প্রতিটি input-এ শুরুর লেখাটি শুধু একবার বসে, তারপর সেটি DOM-এর নিজের জিনিস। “তালিকা উল্টান” চেপে দেখুন, ইচ্ছা হলে আগে কোনো input-এ কিছু লিখে নিন।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const start = [
{ id: 'p1', name: 'চাল', qty: '5 কেজি' },
{ id: 'p2', name: 'ডাল', qty: '2 কেজি' },
{ id: 'p3', name: 'তেল', qty: '1 লিটার' },
];
function Row({ item }) {
return <li>{item.name}: <input defaultValue={item.qty} size="8" /></li>;
}
function App() {
const [items, setItems] = useState(start);
useEffect(() => {
document.querySelectorAll('ul').forEach((ul, n) => console.log(n ? 'id :' : 'index:',
[...ul.children].map(li => li.textContent + li.firstElementChild.value).join(' / ')));
}, [items]);
return (
<div>
<button onClick={() => setItems([...items].reverse())}>তালিকা উল্টান</button>
<h4>key = index</h4>
<ul>{items.map((item, i) => <Row key={i} item={item} />)}</ul>
<h4>key = id</h4>
<ul>{items.map(item => <Row key={item.id} item={item} />)}</ul>
</div>
);
}
createRoot(document.getElementById('root')).render(<App />);
</script>শুরুতে আর একবার বোতাম চাপার পরে পাতার আসল আউটপুট:
index: চাল: 5 কেজি / ডাল: 2 কেজি / তেল: 1 লিটার
id : চাল: 5 কেজি / ডাল: 2 কেজি / তেল: 1 লিটার
index: তেল: 5 কেজি / ডাল: 2 কেজি / চাল: 1 লিটার
id : তেল: 1 লিটার / ডাল: 2 কেজি / চাল: 5 কেজি
উল্টানোর পরে ওপরের list বলছে “তেল: 5 কেজি”, অথচ ৫ কেজি ছিল চালের। নিচের list ঠিক আছে। useEffect এখানে শুধু পাতা থেকে মান পড়ে console-এ লেখার জন্য; এটি পর্ব ১৩-এর বিষয়।
কেন এমন হলো? নিচের ধাপগুলো দুটি পণ্য নিয়ে একই ঘটনা ধীরে দেখায়। প্রতিটি ধাপে খেয়াল করুন, input-টি (A বা B) কার সাথে জোড়া থাকছে।
items: [চাল(p1), ডাল(p2)] key 0 → লেবেল “চাল” → input A: 5 কেজি key 1 → লেবেল “ডাল” → input B: 2 কেজি
items: [ডাল(p2), চাল(p1)]
key 0 → নতুন data: ডাল
key 1 → নতুন data: চাল
আগের key 0 ↔ নতুন key 0: মিলেছে, input A থাকবে
আগের key 1 ↔ নতুন key 1: মিলেছে, input B থাকবে
বদলাবে শুধু লেবেলের লেখা
key 0 → লেবেল “ডাল” → input A: 5 কেজি ✗ ডাল ছিল 2 key 1 → লেবেল “চাল” → input B: 2 কেজি ✗ চাল ছিল 5
items: [চাল(p1), ডাল(p2)] key p1 → লেবেল “চাল” → input A: 5 কেজি key p2 → লেবেল “ডাল” → input B: 2 কেজি
items: [ডাল(p2), চাল(p1)] নতুন প্রথম সারি key p2 ↔ আগের key p2: input B নতুন দ্বিতীয় সারি key p1 ↔ আগের key p1: input A DOM node দুটির ক্রম বদলাবে
key p2 → লেবেল “ডাল” → input B: 2 কেজি ✓ key p1 → লেবেল “চাল” → input A: 5 কেজি ✓
- প্রথম render, key = index. চালের সারির key 0, ডালের সারির key 1. ব্যবহারকারী input A-তে “5 কেজি”, input B-তে “2 কেজি” লিখেছেন। এই লেখা React-এর state-এ নেই, আছে DOM-এর input-এ।
- তালিকা উল্টানো হলো। নতুন array-তে ডাল আগে, চাল পরে। কিন্তু
map-এর index তো আবার 0 আর 1 থেকেই শুরু হয়। - React key মেলাচ্ছে। তার চোখে key 0 আগেও ছিল, এখনো আছে। তাই সে ধরে নেয় একই সারি, শুধু props বদলেছে। Input A আর B যেখানে ছিল সেখানেই থাকে।
- ফল ভুল। লেবেল বদলেছে, input বদলায়নি। ডালের পাশে 5 কেজি, চালের পাশে 2 কেজি। কোনো error নেই, শুধু অর্ডারে ভুল পরিমাণ যাবে। এই বাগ চুপচাপ ক্ষতি করে।
- এবার key = id. শুরুটা একই, শুধু key এখন p1 আর p2. এই key পণ্যের সাথে বাঁধা, অবস্থানের সাথে নয়।
- React key মেলাচ্ছে। নতুন প্রথম সারির key p2. আগের p2-এর সাথে ছিল input B, তাই সেটিই এখন প্রথমে আসবে। এখানে React লেবেল বদলায় না, DOM node-এর ক্রম বদলায়।
- ফল ঠিক। ডালের পাশে 2 কেজি, চালের পাশে 5 কেজি। Input-এর focus, লেখা আর সারির ভেতরের state, সবই পণ্যের সাথে সরে গেছে।
Filter করলেও একই বাগ হয়, কারণ মাঝের সারি বাদ পড়লে পরের সবার index এক ঘর কমে। শুরুতে নতুন সারি যোগ করলে সবার index বাড়ে। তবে index সব সময়ে অপরাধী নয়। তিনটি শর্ত একসাথে মিললে এটি নিরাপদ: list কখনো সাজানো, ছাঁকা বা মাঝখানে যোগ-বিয়োগ হয় না; সারিতে নিজস্ব state নেই; আর সারিতে input বা অন্য DOM state নেই। যেমন ফুটারের স্থির লিংকের তালিকা।
স্থায়ী id কোথা থেকে আসবে
| উৎস | মান | মন্তব্য |
|---|---|---|
| ডেটাবেসের primary key | ✓ সবচেয়ে ভালো | স্বভাবতই অনন্য আর স্থায়ী |
| কয়েকটি ঘর জুড়ে | ✓ ভালো | `${orderId}-${lineNo}`, যদি একা কোনো ঘর অনন্য না হয় |
| তৈরির মুহূর্তে বানানো id | ✓ ভালো | নতুন সারি যোগ করার handler-এ crypto.randomUUID() বা counter, তারপর data-র সাথে সংরক্ষণ |
| পণ্যের নাম | △ ঝুঁকি | দুই দোকানের “চাল” একই নাম হতে পারে, নাম সম্পাদনাও হতে পারে |
| Index | △ শর্তসাপেক্ষ | ওপরের তিন শর্ত মিললে শুধু তখন |
Math.random() বা render-এ বানানো uuid | ✗ ভুল | প্রতি render-এ নতুন key, সব সারি নতুন করে তৈরি |
useId | ✗ ভুল | React-এর নথিতে নিষেধ; key আসবে data থেকে |
Math.random()-এর ক্ষতি মেপে দেখেছি। সত্যিকারের run (React 19.3.1, Edge): input-এ abc লিখে পাশের একটি বোতাম চাপা হলো, যা শুধু একটি counter state বাড়ায়। Render-এর পরে সেই input-এর মান হলো খালি। কারণ নতুন key মানে পুরোনো input মুছে নতুন input বসানো। Focus-ও সাথে সাথে হারিয়ে যায়।
একটি সূক্ষ্ম কথা: input-এর মান যদি parent-এর state-এ id ধরে থাকে (পর্ব ১২-এর controlled input), তাহলে লেখা ভুল সারিতে যাবে না। কিন্তু index key থাকলে focus, সারির ভেতরের অন্য state আর animation তবুও ভুল জায়গায় থাকবে। তাই “আমার input controlled” বলে index রেখে দেওয়া নিরাপদ অভ্যাস নয়।
৪ · Filter: state নয়, হিসাব করা মান
এবার “বাজারঘর”-এ search বসাই। নতুনদের প্রথম চিন্তা হয়: filteredProducts নামে আরেকটি state বানাই, আর search বদলালে সেটি আপডেট করি। এতে একই তথ্যের দুটি কপি হলো। কোনো একটি জায়গায় আপডেট ভুলে গেলেই পর্দা আর data আলাদা কথা বলবে।
চায়ের দোকানের হিসাব খাতার কথা ভাবুন। খাতায় প্রতিদিনের বিক্রি লেখা থাকে, আর মাসের মোট বের হয় যোগ করে। মোটটিকে আলাদা খাতায় লিখে রাখলে কোনো একটি ভুল সংশোধনের দিন দুই খাতা আর মিলবে না। State হলো মূল খাতা। Filter করা list হলো মোট: দরকারের সময়ে হিসাব করে নিলেই হয়।
নিয়মটি এই: যা অন্য state বা props থেকে হিসাব করা যায়, সেটি state-এ রাখার দরকার নেই। এমন মানকে বলে derived value. State থাকবে শুধু দুটি: খোঁজার লেখা আর category. বাকিটা প্রতি render-এ হিসাব হবে।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const products = [
{ id: 'p1', name: 'মিনিকেট চাল', cat: 'মুদি' },
{ id: 'p2', name: 'মসুর ডাল', cat: 'মুদি' },
{ id: 'p3', name: 'চায়ের কেটলি', cat: 'রান্নাঘর' },
{ id: 'p4', name: 'হাঁড়ি', cat: 'রান্নাঘর' },
{ id: 'p5', name: 'খাতা', cat: 'স্টেশনারি' },
];
const clean = s => s.normalize('NFC').trim().toLowerCase();
const cats = ['সব', ...new Set(products.map(p => p.cat))];
function Shop() {
const [query, setQuery] = useState('');
const [cat, setCat] = useState('সব');
const visible = products.filter(p =>
(cat === 'সব' || p.cat === cat) && clean(p.name).includes(clean(query)));
console.log(`cat=${cat} query="${query}" -> ${visible.length}/${products.length}`);
return <div>
<input aria-label="পণ্য খুঁজুন" value={query} onChange={e => setQuery(e.target.value)} />
{cats.map(c => <button key={c} aria-pressed={c === cat} onClick={() => setCat(c)}>{c}</button>)}
<p role="status">{visible.length}টি পণ্য</p>
{visible.length > 0
? <ul>{visible.map(p => <li key={p.id}>{p.name}</li>)}</ul>
: <p>কিছু পাওয়া যায়নি। অন্য শব্দ লিখে দেখুন।</p>}
</div>;
}
createRoot(document.getElementById('root')).render(<Shop />);
</script>“রান্নাঘর” চেপে, তারপর খোঁজার ঘরে “চা” লিখে, শেষে “ঝ” লিখলে পাতার আসল আউটপুট:
cat=সব query="" -> 5/5
cat=রান্নাঘর query="" -> 2/5
cat=রান্নাঘর query="চা" -> 1/5
cat=রান্নাঘর query="ঝ" -> 0/5
প্রতি লাইন একটি render. visible কোথাও জমা নেই, প্রতিবার নতুন করে তৈরি হচ্ছে। শেষ লাইনে 0টি পণ্য, তাই পর্দায় খালি অবস্থার বার্তা। পণ্যের key p.id, category বোতামের key নামটি নিজেই, কারণ category-র নাম এখানে অনন্য।
1const [cat, setCat] = useState('সব');2const [query, setQuery] = useState('');3const visible = products.filter(...);4return <List items={visible} />;
| cat | |
| query | |
| visible |
- প্রথম render শুরু।
cat-এর শুরুর মান “সব”। queryফাঁকা। State মাত্র এই দুটি।filterচলল। ফাঁকা লেখা সব নামের মধ্যেই আছে, তাই পাঁচটি পণ্যইvisible-এ এলো।- “রান্নাঘর” চাপলেন।
setCatনতুন render চাইল। নতুন render-এcatবদলেছে।visibleএই render-এ এখনো হিসাব হয়নি। আগের render-এর পর্দা তখনো দেখা যাচ্ছে। filterআবার চলল। p3 আর p4 রান্নাঘরের, তাই দুটি পণ্য। আলাদা কোনো “আপডেট” কোড লিখতে হয়নি।- “চা” লিখলেন। শুধু
queryবদলাল,catআগের মতোই। - দুই শর্ত একসাথে মিলল: রান্নাঘরের পণ্য, আর নামে “চা”। শুধু p3 “চায়ের কেটলি”। “মিনিকেট চাল”-এও “চা” আছে, কিন্তু সেটি মুদি।
- লেখা মুছে “ঝ” লিখলেন। আবার নতুন render.
- কোনো পণ্যের নামে “ঝ” নেই।
visibleখালি, তাই পর্দায় empty state. Data, filter আর পর্দা কখনো আলাদা হতে পারেনি, কারণvisible-এর কোনো দ্বিতীয় কপি নেই।
বাংলা search-এর একটি ফাঁদ: “য়” অক্ষরটি Unicode-এ দুইভাবে লেখা যায়: একটি code point (U+09DF), অথবা য আর নুক্তা মিলিয়ে (U+09AF U+09BC) দুই code point. দেখতে এক, কিন্তু সরাসরি তুলনায় আলাদা। Node.js-এ পরীক্ষা করে দেখেছি: includes সরাসরি false দিয়েছে, দুই পাশে normalize('NFC') করলে true. ড় আর ঢ়-এও একই বিষয়। তাই ওপরের clean ফাংশনে normalize আছে। বাংলা সাইটের বাকি কথা পর্ব ৫৯-এ।
খরচ নিয়ে ভয় পাওয়ার আগে হিসাব করুন
“প্রতি render-এ filter চললে ধীর হবে না?” হিসাব করি। ধরুন ১০,০০০ পণ্য। প্রতিটি অক্ষর লিখলে ১০,০০০টি তুলনা, “চাল” তিন অক্ষরের মতো টাইপে ৩০,০০০. Sort করলে প্রায় n log₂ n = ১০,০০০ × ১৩.২৯ ≈ ১.৩৩ লাখ তুলনা। আধুনিক ব্রাউজারে সাধারণ তুলনায় এটি সাধারণত সমস্যা নয়। React-এর নথির পরামর্শও তাই: আগে মাপুন, ধীর প্রমাণ হলে তবে useMemo (পর্ব ১৪)। এটি কেবল হিসাব বাঁচায়; state-এ কপি রাখার যুক্তি কোনোভাবেই নয়। আর এক লাখ পণ্যের দোকানে সব পণ্য ব্রাউজারে আনাই ভুল: তখন filter হবে সার্ভারে, পর্ব ৩৪-এ দেখবেন।
৫ · Sort, grouping আর nested list
Sort-ও derived value. State-এ থাকবে শুধু “কোন কলাম, কোন দিকে”; সাজানো list হিসাব হবে। একটি ফাঁদ আছে: Array.prototype.sort মূল array-কেই বদলে দেয়। State বা props-কে সরাসরি sort করলে React-এর হাতের data বদলে যায়, যা পর্ব ১০-এর নিয়মের বাইরে। তাই আগে কপি করুন: [...rows].sort(...), অথবা নতুন array দেওয়া rows.toSorted(...).
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const rows = [
{ id: 'p1', name: 'চাল', price: 85, stock: 40 },
{ id: 'p2', name: 'ডাল', price: 140, stock: 12 },
{ id: 'p3', name: 'তেল', price: 190, stock: 25 },
{ id: 'p4', name: 'খাতা', price: 60, stock: 12 },
];
const cols = [['name', 'পণ্য'], ['price', 'দাম'], ['stock', 'মজুত']];
const compare = (a, b, k) => typeof a[k] === 'number' ? a[k] - b[k] : a[k].localeCompare(b[k], 'bn');
function Table() {
const [sort, setSort] = useState({ key: 'price', dir: 1 });
const sorted = [...rows].sort((a, b) => sort.dir * compare(a, b, sort.key));
console.log(sort.key, sort.dir > 0 ? 'asc :' : 'desc:', sorted.map(r => r.id).join(' '));
const toggle = key => setSort(s => ({ key, dir: s.key === key ? -s.dir : 1 }));
const aria = k => sort.key !== k ? undefined : sort.dir > 0 ? 'ascending' : 'descending';
return <table>
<caption>বাজারঘরের মজুত</caption>
<thead><tr>{cols.map(([k, label]) => <th key={k} scope="col" aria-sort={aria(k)}>
<button onClick={() => toggle(k)}>{label}{sort.key === k && (sort.dir > 0 ? ' ▲' : ' ▼')}</button>
</th>)}</tr></thead>
<tbody>{sorted.map(r => <tr key={r.id}><td>{r.name}</td><td>{r.price}</td><td>{r.stock}</td></tr>)}</tbody>
</table>;
}
createRoot(document.getElementById('root')).render(<Table />);
</script>“দাম”, তারপর “মজুত”, তারপর আবার “মজুত” চাপলে পাতার আসল আউটপুট:
price asc : p4 p1 p2 p3
price desc: p3 p2 p1 p4
stock asc : p2 p4 p3 p1
stock desc: p1 p3 p2 p4
দুটি জিনিস দেখুন। সংখ্যার তুলনা বিয়োগ দিয়ে, বাংলা লেখার তুলনা localeCompare দিয়ে। আর মজুত সমান হলে p2 আর p4-এর আগের ক্রম রয়ে গেছে, কারণ JavaScript-এর sort stable. উল্টো দিকেও একই, কারণ এখানে শুধু তুলনার চিহ্ন উল্টানো হয়েছে।
Grouping: category ধরে ভাগ
Group করাও হিসাব। Object.groupBy একটি array-কে category ধরে ভাগ করে। MDN অনুযায়ী এটি ২০২৪ সালের মার্চ থেকে সব বড় ব্রাউজারে আছে; খুব পুরোনো ফোন সমর্থন করতে হলে reduce বা Map ব্যবহার করুন। List-এর ভেতরে list হলে প্রতিটি স্তরে আলাদা key লাগে:
const groups = Object.groupBy(products, p => p.cat);
return Object.entries(groups).map(([cat, items]) => (
<section key={cat}>
<h3>{cat}</h3>
<ul>
{items.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
</section>
));এখানে বাইরের key category, ভেতরের key পণ্য। দুই স্তরের key মিলে গেলেও সমস্যা নেই, কারণ key শুধু ভাইবোনদের মধ্যে অনন্য হলেই চলে। প্রতিটি উপাদান থেকে একাধিক সারি দরকার হলে Fragment লাগে, যেমন <dl>-এর <dt> আর <dd>. ছোট রূপ <>-এ key বসে না, তাই পুরো রূপ লিখতে হয়: <Fragment key={f.id}>.
ভেতরের list বড় হলে আলাদা component করুন: <CategoryBlock key={cat} items={items} />. কোড পড়া সহজ হয়, আর প্রতিটি অংশের নিজস্ব state থাকতে পারে, যেমন “ভাঁজ করা”। পর্ব ০৯-এর composition এখানেই কাজে লাগে।
৬ · শর্তসাপেক্ষ UI: &&, ternary না early return
“স্টকে থাকলে কিনুন বোতাম, না থাকলে ‘শেষ’ লেখা।” এই ধরনের কাজকে বলে conditional rendering. React-এ এর জন্য বিশেষ কোনো syntax নেই, সবই সাধারণ JavaScript (শর্ত আর truthy-falsy মনে করে নিন)। প্রশ্ন শুধু কোন রূপটি কোথায় পড়তে সহজ। নিচের ধাপগুলো সিদ্ধান্তের একটি ক্রম।
- পুরো component?
if … returnearly return - দুই রূপের একটি?
a ? x : yternary - থাকবে বা থাকবে না?
ok && xboolean-সহ - দুইয়ের বেশি রূপ?
আলাদা componentবা object map
function ProductPage({ product }) {
if (!product) return <p>পণ্যটি পাওয়া যায়নি।</p>;
return <h1>{product.name}</h1>;
}
<p>
{product.name}:
{stock > 0 ? <button>কিনুন</button> : <b>শেষ</b>}
</p>
const count = 3; {count > 0 && <Badge n={count} />} পর্দায়: Badge, 3 দেখাচ্ছে count 0 হলে: বাঁ পাশ false, পর্দায় কিছু নেই
const count = 0; {count && <Badge n={count} />} পর্দায়: 0
const badge = {
paid: <b>পরিশোধিত</b>,
pending: <b>অপেক্ষায়</b>,
failed: <b>ব্যর্থ</b>,
};
return <p>{badge[order.status]}</p>;
- প্রথম প্রশ্ন: পুরো component কি অন্য কিছু দেখাবে? হ্যাঁ হলে early return. ব্যতিক্রমের ঘটনা শুরুতেই শেষ হয়, মূল কোড কম ভেতরে সরে। নিচের কোডে
productনিশ্চিত থাকে; TypeScript-ও এখানে narrowing করে। - দ্বিতীয় প্রশ্ন: JSX-এর ছোট একটি জায়গায় দুই রূপের একটি? তাহলে ternary. দুই পাশ ছোট থাকলে পড়তে ভালো। Ternary-র ভেতরে ternary হলে আর নয়।
- তৃতীয় প্রশ্ন: একটি জিনিস থাকবে বা থাকবে না? তাহলে
&&. বাঁ পাশে স্পষ্ট boolean রাখুন।count > 0সবসময়ে true বা false দেয়। - ফাঁদ। বাঁ পাশ
0হলে short-circuit-এর নিয়মে পুরো expression-এর মান0. আর React সংখ্যা আঁকে, তাই পর্দায় একটি একলা 0 ওঠে।list.length && …ঠিক এই বাগ। - দুইয়ের বেশি রূপ? JSX-এ শিকল বানাবেন না। Component-এর ওপরে
if, একটি object map, অথবা আলাদা component ব্যবহার করুন। React-এর নথিও জটিল শর্তের জন্য child component আলাদা করতে বলে।
{ }-এর ভেতরে কোন মান কী আঁকে, সেটি জানা থাকলে অনেক অবাক হওয়া বাঁচে। সত্যিকারের run, react-dom/server 19.3.1:
{ }-এর মান | পর্দায় |
|---|---|
0 | 0 |
NaN | NaN |
'' | কিছু না |
false, true | কিছু না |
null, undefined | কিছু না |
'চাল' | চাল |
[1, 2] | 12 |
Component থেকে null ফেরত দেওয়া বৈধ, কিছুই আঁকা হবে না। তবে React-এর নথি বলে, এটি পড়তে গিয়ে অবাক করতে পারে। বেশির ভাগ সময়ে শর্তটি parent-এ রাখাই পরিষ্কার: {isAdmin && <AdminPanel />}. একটি বড় কথা মনে রাখুন: UI লুকানো নিরাপত্তা নয়। বোতাম না দেখালেও API খোলা থাকলে যে কেউ ডাকতে পারে। আসল যাচাই সার্ভারে, পর্ব ৪৮-এ দেখবেন।
৭ · Empty, loading, error: তিনটি অবস্থার অভ্যাস
নতুন developer শুধু “সব ঠিক আছে” অবস্থাটি বানান। অথচ আসল ব্যবহারকারী ধীর নেটওয়ার্কে থাকেন, সার্ভার মাঝে মাঝে ভাঙে, আর নতুন দোকানে পণ্যই থাকে না। তাই যেকোনো list বানানোর সময়ে নিজেকে চারটি প্রশ্ন করুন: আনার সময়ে কী দেখাবে? ব্যর্থ হলে? খালি হলে? ঠিক থাকলে?
Loading
কিছু হচ্ছে, এটি জানান। কাঠামো জানা থাকলে পাতা না লাফিয়ে ফাঁকা সারির ছাঁচ (skeleton) দেখাতে পারেন, এতে CLS কমে।
লেখায়ও জানানEmpty
“কিছু নেই” বলে থামবেন না। কারণ আর পরের পদক্ষেপ বলুন: “কার্ট খালি। পণ্য দেখুন।”
পরের পথ দেখানError
কী হয়েছে, সহজ ভাষায় বলুন, আর “আবার চেষ্টা” বোতাম দিন। ভেতরের প্রযুক্তিগত বার্তা ব্যবহারকারীকে দেখাবেন না।
বের হওয়ার পথএখন প্রশ্ন, এই অবস্থাগুলো কোথায় রাখবেন? সহজ চিন্তা হলো তিনটি boolean: isLoading, isError, isEmpty. হিসাব করি। তিনটি boolean মানে 2³ = ৮টি মিশ্রণ। বৈধ মাত্র ৪টি: সব false (সফল), অথবা ঠিক একটি true. বাকি ৪টি অসম্ভব অবস্থা, যেমন একই সাথে loading আর error. কোড ভুলে সেখানে চলে গেলে পর্দায় কী দেখাবে, কেউ জানে না। সমাধান একটি মান: status, যার ৪টি রূপ, ৪টিই বৈধ। পর্ব ০৬-এর discriminated union ঠিক এই ধারণাকেই type-এ বাঁধে।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const items = [{ id: 'p1', name: 'চাল' }, { id: 'p2', name: 'ডাল' }];
const statuses = ['loading', 'empty', 'error', 'success'];
function View({ status, onRetry }) {
if (status === 'loading') return <p>পণ্য আনা হচ্ছে…</p>;
if (status === 'error') return <p>পণ্য আনা যায়নি।
<button onClick={onRetry}>আবার চেষ্টা করুন</button></p>;
if (status === 'empty') return <p>এই দোকানে এখনো পণ্য নেই।</p>;
return <ul>{items.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
function App() {
const [status, setStatus] = useState('loading');
console.log('status =', status);
return <div>
{statuses.map(s => (
<button key={s} aria-pressed={s === status} onClick={() => setStatus(s)}>{s}</button>
))}
<div role="status">
<View status={status} onRetry={() => setStatus('loading')} />
</div>
</div>;
}
createRoot(document.getElementById('root')).render(<App />);
</script>“error”, তারপর “আবার চেষ্টা করুন”, তারপর “empty”, শেষে “success” চাপলে পাতার আসল আউটপুট:
status = loading
status = error
status = loading
status = empty
status = success
একই component, একটি মান, এক সময়ে ঠিক একটি শাখা। View-তে early return-গুলো পড়লেই সব অবস্থার তালিকা পাওয়া যায়। বোতামগুলো এখানে শেখার জন্য; আসল অ্যাপে status আসে network অনুরোধ থেকে (পর্ব ১৩ আর ১৯)। role="status"-এর ঘরটি সব সময়ে পাতায় থাকে, ভেতরের লেখা বদলায়; এর কারণ অংশ ৯-এ।
খালি আর ব্যর্থ এক জিনিস নয়। catch-এ setItems([]) করলে সার্ভার ভাঙলেও ব্যবহারকারী দেখবেন “কোনো পণ্য নেই”। তিনি ভাববেন দোকান বন্ধ। এই লুকানো ব্যর্থতা ধরা সবচেয়ে কঠিন।
৮ · key দিয়ে component নতুন করে শুরু
Key শুধু list-এর জন্য নয়। React state মনে রাখে গাছের অবস্থান ধরে। ধরুন পণ্যের পাতায় একটি রিভিউ ফর্ম আছে। ব্যবহারকারী চালের রিভিউ অর্ধেক লিখে পাশের ডালে গেলেন। Component একই জায়গায় থাকায় React লেখাটি রেখে দেয়, আর চালের রিভিউ ডালের ফর্মে চলে আসে।
function ProductPage({ product }) {
return (
<>
<h1>{product.name}</h1>
<ReviewForm key={product.id} product={product} />
</>
);
}এখন product.id বদলালে React দুটিকে আলাদা component ধরে। পুরোনোটির state আর DOM মুছে নতুনটি শুরু থেকে তৈরি হয়। এটি effect দিয়ে একটি একটি করে state খালি করার চেয়ে অনেক পরিষ্কার। React-এর ২০১৮ সালের ব্লগেও (“You Probably Don't Need Derived State”, Brian Vaughn) এই কৌশলকে বেশির ভাগ ক্ষেত্রে সেরা বলা হয়েছে।
এর খরচও আছে: পুরো গাছ নতুন করে তৈরি হয়, ভেতরের সব state আর focus যায়। তাই key-টি অর্থপূর্ণ কিছু হওয়া চাই, যেমন “এটি অন্য পণ্যের ফর্ম”। একটি কাজের রূপ: key={attempt}. ব্যর্থ একটি অংশ “আবার চেষ্টা” চাপলে attempt এক বাড়ালেই পুরোটা নতুন করে শুরু হয়।
আরও গভীরে: React-এর list তুলনা দুই ধাপে চলে। প্রথমে নতুন আর পুরোনো সন্তানদের পাশাপাশি হাঁটে, key মিললে এগোয়। প্রথম অমিলে বাকি পুরোনো সন্তানদের key ধরে একটি Map বানায়, আর প্রতিটি নতুন সন্তান সেখানে খোঁজে। কোন node সরাতে হবে, সেটি ঠিক হয় পুরোনো অবস্থানের একটি হিসাব দিয়ে। তাই প্রথম সারি শেষে পাঠালে একটি node সরলেই চলে, কিন্তু শেষ সারি শুরুতে আনলে বাকি সবাইকে সরাতে হয়। বিস্তারিত পর্ব ১৫-এ।
৯ · লম্বা list আর সবার জন্য list
Virtualization: শুধু যা দেখা যায়
১০,০০০ সারি একসাথে DOM-এ রাখলে কী হয়? এক সারিতে ৪টি element (li, নাম, দাম, বোতাম) ধরে আমরা Edge-এ একটি production build মেপেছি। এটি এক মেশিনের স্থানীয় ফল, ৭ বারের মধ্যক (ধীর CPU-র ঘরে ৫ বারের); সাধারণ নিয়ম নয়। মাপে আছে শুধু React-এর render আর DOM-এ বসানো, layout আর paint নেই।
| সারি | DOM element | সময় | CPU ৪ গুণ ধীর করে (emulation) |
|---|---|---|---|
| ২৫ | ১০১ | ১ ms | ৬ ms |
| ১,০০০ | ৪,০০১ | ৭ ms | মাপা হয়নি |
| ১০,০০০ | ৪০,০০১ | ৪২ ms | ১৭৯ ms |
ধীর CPU-র ঘরটি দেখুন। ভালো INP-এর সীমা ২০০ ms, আর শুধু এই কাজেই ১৭৯ ms গেল। Virtualization-এর ধারণা সরল: যা পর্দায় দেখা যায় শুধু সেটুকু আঁকুন, বাকিটার জন্য খালি জায়গা রাখুন।
মুদি দোকানের গুদামে হাজার বস্তা, কিন্তু সামনের তাকে থাকে কয়েকটি। ক্রেতা যেদিকে তাকান, দোকানি সেই পণ্য তাকে তোলেন। ক্রেতার মনে হয় সব সামনেই আছে।
হিসাব: সারির উচ্চতা ৪০ px, দেখার জায়গা ৬০০ px, মোট ১০,০০০ সারি। দৃশ্যমান সারি ৬০০ ÷ ৪০ = ১৫. ওপরে-নিচে ৫টি করে বাড়তি সারি (overscan), যাতে দ্রুত scroll-এ ফাঁকা না দেখায়। মোট আঁকা হয় ১৫ + ৫ + ৫ = ২৫টি, মানে ১০১টি element, ৪০,০০১-এর জায়গায়। ভেতরের ফাঁকা ঘরের উচ্চতা ১০,০০০ × ৪০ = ৪,০০,০০০ px, যাতে scrollbar ঠিক দেখায়। ১,২০,০০০ px scroll করলে প্রথম দৃশ্যমান সারি ১,২০,০০০ ÷ ৪০ = ৩,০০০, আঁকা হয় ২,৯৯৫ থেকে ৩,০১৯.
হাতে লেখার দরকার সাধারণত হয় না। react-window (লেখার সময়ে 2.5.2) আর TanStack Virtual (@tanstack/react-virtual 3.14.25) দুটি পরিচিত উদাহরণ; API version ধরে বদলায়, তাই তাদের নথি দেখে নিন। লাইব্রেরি নেওয়ার আগে তিনটি প্রশ্ন করুন: সারি কি সত্যিই শত শত? Pagination (পর্ব ৩৪) বা CSS-এর content-visibility: auto-তে কি কাজ চলে? ব্যবহারকারী কি পাতায় Ctrl+F দিয়ে খোঁজেন? Virtualization-এ যে সারি আঁকা হয়নি, ব্রাউজারের খোঁজ সেটি পায় না। Screen reader-ও মোট সংখ্যা জানে না, যদি আপনি aria-setsize আর aria-posinset না দেন।
সবার জন্য list
Screen reader ব্যবহারকারী চোখে পাতা দেখেন না; তিনি শোনেন। আসল semantic HTML দিলে “list, ৫টি আইটেম” শোনা যায়। তার ওপর একটু যত্ন (ভিত্তি আছে accessibility আর list ও table পর্বে):
- আসল element: list-এ
ul,ol,li; তুলনার data-য়table,caption,th scope. সবdivদিলে গঠনের তথ্য হারায়। - Bullet সরালে: Safari আর VoiceOver-এ
list-style: noneদিলে list হিসেবে না শোনাতে পারে, এমন আচরণ নথিবদ্ধ আছে। প্রয়োজনেrole="list"দিয়ে নিজে পরীক্ষা করুন। - ফলের সংখ্যা জানান: Filter বদলালে চোখে দেখা যায়, কানে শোনা যায় না।
role="status"একটি polite live region. ঘরটি আগে থেকেই পাতায় থাকতে হয়, ভেতরের লেখা বদলালে তবেই নির্ভরযোগ্যভাবে শোনানো হয়। - Sort-এর অবস্থা: কলামের নাম
button-এ, আর চলতি কলামেরth-তেaria-sort. WAI-ARIA Authoring Practices-এর sortable table উদাহরণ এই ধরনই দেখায়। - Error: জরুরি বার্তায়
role="alert"দেওয়া যায়। শুধু রঙে নয়, লেখায়ও বলুন। - Focus: সারি মুছলে focus
body-তে পড়ে যেতে পারে, keyboard ব্যবহারকারী পাতার শুরুতে চলে যান। মোছার পরে focus পাশের সারিতে বা list-এর শিরোনামে আনুন। স্থায়ী key থাকলে সরে যাওয়া সারিতে focus ঠিক থাকে।
পরীক্ষা ও ইন্টারভিউতে:
- React-এ key কেন লাগে? Render-এর মধ্যে কোন সারি কোনটি, সেটি চিনতে। এতে state আর DOM ঠিক data-র সাথে থাকে।
- Index key-এর সমস্যা কী? ক্রম বদলালে index একই থাকে, তাই DOM আর state ভুল সারিতে থাকে।
count && <X />-এ 0 দেখায় কেন?&&বাঁ পাশের মান0ফেরত দেয়, আর React সংখ্যা আঁকে।- Filter করা list state-এ রাখবেন? সাধারণত না; render-এ হিসাব করুন, ধীর হলে
useMemo. - Component-এর সব state কীভাবে reset করবেন? ভিন্ন
keyদিয়ে, যা বিশেষ করে পরিচয় বদলালে কাজে লাগে।
১০ · AI-এর যুগে list আর শর্ত
List-এর কোড AI কয়েক সেকেন্ডে লেখে, আর প্রথম দেখায় ঠিকই চলে। সমস্যা ধরা পড়ে সারি সরালে, list খালি হলে, নেটওয়ার্ক ভাঙলে। তাই যাচাই মানে কোড পড়া নয়, ওই পরিস্থিতিগুলো ঘটানো।
| AI যা লেখে | সমস্যা | যেভাবে ধরবেন |
|---|---|---|
key={index} বা render-এ uuid() | Input ভুল সারিতে, অথবা প্রতিবার সব নতুন | Input-এ লিখে list উল্টান |
useEffect দিয়ে filter state আপডেট | দুই কপি, বাড়তি render | State-এ এমন কিছু খুঁজুন যা হিসাব করা যায় |
items.length && … | খালি list-এ পর্দায় 0 | খালি array দিয়ে চালান |
Render-এ items.sort() | মূল state বদলে যায় | Sort-এর আগে কপি আছে কি না দেখুন |
catch-এ setItems([]) | Error লুকিয়ে খালি list দেখায় | Network বন্ধ করে পাতা খুলুন |
Client-এ orders.filter(o => o.userId === me) | API সবার অর্ডার পাঠাচ্ছে, authorization নেই | DevTools-এর Network ট্যাবে response দেখুন |
| ৫০ সারির জন্য নতুন virtualization বা group লাইব্রেরি | অকারণ dependency | আগে মাপুন, Object.groupBy আছে কি না দেখুন |
পুরোনো class-এর componentWillReceiveProps দিয়ে reset | React-এ UNSAFE বলে চিহ্নিত পুরোনো পথ | আপনার version-এর react.dev দেখুন, key ব্যবহার করুন |
কোড চাওয়ার সময়ে অবস্থাগুলো নিজেই বলে দিন, আর ব্যাখ্যা চান। এতে AI সহজ পথে গিয়ে ফাঁক রেখে দিতে পারে না:
AI-এর লেখা list হাতে পেলে: যাচাইয়ের তালিকা
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১React নিজেই সারি চিনে নেয় না কেন? Key-এর দায়িত্ব developer-এর ওপর ছাড়া হলো কেন?
কারণ সাধারণ অর্থে দুটি list মেলানো একটি অনুমানের কাজ। React দেখে দুই render-এর দুটি অবস্থা। মাঝখানে কী ঘটেছে, সে জানে না। ধরুন আগে ছিল [চাল, ডাল], পরে [ডাল, চাল]। এটি কি দুটি সারির জায়গা বদল, নাকি দুটি সারির লেখা বদল? দুটিই একই পর্দা বানায়, কিন্তু input-এর জন্য ফল আলাদা। শুধু data-র মালিক জানেন “এই সারি কোন জিনিস”।
React-এর পুরোনো নথিতে খরচের দিকটি বলা আছে: দুটি গাছের পূর্ণ তুলনার সেরা পরিচিত পদ্ধতির খরচ O(n³)। ১,০০০ element-এ সেটি প্রায় ১০০ কোটি তুলনা, যা প্রতি render-এ চলতে পারে না। তাই React দুটি অনুমান নিয়ে O(n) পদ্ধতি বেছে নেয়, আর দ্বিতীয় অনুমানটিই key. নিখুঁত উত্তরের বদলে খুব দ্রুত আর অনুমানযোগ্য একটি উত্তর নেওয়া হলো, বিনিময়ে developer একটু তথ্য দেবেন।
অন্য framework-ও একই সমস্যায় পড়েছে, আর নকশার তফাত শিক্ষণীয়। Vue-র নথি বলে, key না থাকলে সে “in-place patch” করে, যা শুধু তখনই উপযুক্ত যখন list child state বা অস্থায়ী DOM state-এর ওপর নির্ভর করে না। Angular-এর নতুন @for-এ track লেখা বাধ্যতামূলক, নথিতে কারণ হিসেবে খারাপ performance-এর ঝুঁকির কথা বলা আছে। অর্থাৎ একটি framework warning দেয়, আরেকটি ঘোষণা ছাড়া compile-ই হতে দেয় না। দুটিই মানে যে এই তথ্য মেশিন নিজে থেকে জানে না। কঠোরতার মাত্রা আলাদা হলেও সমস্যাটি সবার কাছে একই, আর তাই নতুন framework শিখতে গেলে “এখানে key-এর মতো জিনিসটি কী?” প্রশ্নটি আপনাকে অনেক দূর এগিয়ে দেবে।
মূল উপলব্ধি: Key কোনো React-এর খামখেয়াল নয়। পরিচয়ের তথ্য data-র মধ্যে থাকে, আর সেটি framework-কে জানানো developer-এর কাজ। যেকোনো UI লাইব্রেরিতেই এই প্রশ্ন ফিরে আসবে।
প্র ০২“Index কখনোই key নয়” কি একটি সত্য নিয়ম, না অতিরিক্ত সরলীকরণ?
নিয়মটির উৎস বাস্তব। ২০১৫ সালের অক্টোবরে Robin Pokorny “Index as a key is an anti-pattern” নামে একটি লেখা প্রকাশ করেন, যা এই সমস্যার পরিচিত উল্লেখ হয়ে ওঠে; পরে কোনো কোনো linter-এর নথিতেও সেই লেখার উল্লেখ দেখা যায়। আজ Oxlint, ESLint-এর React plugin আর Biome-এ এমন নিয়ম আছে।
কিন্তু “কখনোই নয়” কথাটি অতিরঞ্জন। React-এর পুরোনো নথি index-কে “শেষ উপায়” বলেছিল, এবং বলেছিল সারি পুনর্বিন্যাস না হলে এটি ভালোই চলে। আজকের নথি বলে, ক্রম বদলালে এটি সূক্ষ্ম বাগ আনে। Angular-এর নথিও স্থির সংগ্রহে track $index-কে গ্রহণযোগ্য বলে। আসল প্রশ্ন “index কি খারাপ?” নয়, প্রশ্ন হলো “এই list-এ অবস্থান কি পরিচয়ের সমান?” ফুটারের স্থির লিংকে অবস্থান আর পরিচয় কখনো আলাদা হয় না, তাই সেখানে ক্ষতি নেই।
এখানে একটি বিরল কৌশলও আছে। পাতা ধরে আসা ফলাফলে সব সারি যদি সম্পূর্ণ stateless হয়, তবে index key-তে React DOM node পুনর্ব্যবহার করে শুধু লেখা বদলায়। এতে কোনো কোনো ক্ষেত্রে কাজ কম হতে পারে। তবে এটি মেপে নেওয়ার বিষয়, ডিফল্ট নয়। সমস্যা হলো, আজ যে list স্থির, কাল তাতে sort বসতে পারে। তখন কেউ মনে রাখবেন না যে key-টি শর্তসাপেক্ষ ছিল, আর linter চুপ থাকলে বাগটি নিঃশব্দে ঢুকবে। তাই দলের নিয়ম হিসেবে “id থাকলে id, ব্যতিক্রম হলে কারণসহ মন্তব্য” বেশ ভালো কাজ করে।
মূল উপলব্ধি: ভালো নিয়মের পেছনের কারণ জানলে ব্যতিক্রমও বোঝা যায়। “কখনো নয়” মুখস্থ না করে শর্তগুলো মনে রাখুন: ক্রম, state আর DOM. একটি নিয়ম কেন আছে জানলে কোড review-তে তর্কও ফলপ্রসূ হয়।
প্র ০৩Filter করা list state-এ রাখলে আসলে কী ক্ষতি? Performance-এর জন্য তো জমিয়ে রাখা ভালো মনে হয়।
দুই রকম ক্ষতি। প্রথমটি সঠিকতার। একই তথ্যের দুটি কপি থাকলে প্রতিটি পরিবর্তনে দুটিই মেলাতে হয়। একটি সারি মুছলেন, মূল list-এ গেল, ফিল্টার করা কপিতে রয়ে গেল: পর্দায় মুছে ফেলা পণ্য দেখা যাচ্ছে। যত বেশি পথে data বদলায়, তত বেশি জায়গায় মেলানোর কোড লিখতে হয়, আর একটি ভুলে গেলেই বাগ। এই ধরনের বাগ একবারে ধরা পড়ে না, কারণ বেশির ভাগ সময়ে কপি দুটি মিলে থাকে।
দ্বিতীয়টি performance-এরই। State কপি মেলাতে সাধারণত useEffect লাগে। তখন ক্রম হয়: state বদলাল, পুরোনো filter নিয়ে render হলো, effect চলল, নতুন state বসল, আবার render. react.dev-এর “You Might Not Need an Effect” ঠিক এই ধরনকে এড়াতে বলে, কারণ render-এ হিসাব করলে এই বাড়তি ধাপ লাগে না। অর্থাৎ যা বাঁচানোর জন্য জমানো হলো, উল্টো সেটিই খরচ বাড়াল।
React দল বিষয়টি নিয়ে ২০১৮ সালের জুনে একটি পুরো ব্লগই লেখে: “You Probably Don't Need Derived State”, লেখক Brian Vaughn. সেখানে বলা হয়, memoization-এর জন্য derived state খারাপ না হলেও সাধারণত সেরা সমাধান নয়। সেই সময়ে পরামর্শ ছিল render-এর ভেতরে filter করা আর ধীর হলে memoize helper. আজকের রূপ useMemo. তবে নথির সতর্কতাটিও মনে রাখুন: useMemo প্রথম render দ্রুত করে না, আর হাজার হাজার object না হলে হিসাব সাধারণত দামি নয়। তাই console.time দিয়ে মেপে তারপর সিদ্ধান্ত।
ব্যতিক্রম আছে। সার্ভার থেকে আসা ফিল্টার করা ফল আসলে নতুন data, derived নয়; সেটি রাখা ঠিক। আর ব্যবহারকারী যদি সম্পাদনার সময়ে “শুরুর মান” ধরে রাখতে চান, সেটিও আলাদা তথ্য।
মূল উপলব্ধি: State-কে সবচেয়ে ছোট সত্য হিসেবে ভাবুন। বাকি সব সেখান থেকে হিসাব হবে। এই ধারণা “single source of truth” নামে ডেটাবেসের normalization-এও দেখা যায়, যেখানে একই তথ্য দুই জায়গায় রাখলে অসংগতির ঝুঁকি বাড়ে।
প্র ০৪Loading, error আর empty-কে তিনটি boolean না রেখে একটি status কেন? এটি কি শুধুই রুচির ব্যাপার?
না, এখানে একটি গণনাযোগ্য যুক্তি আছে। প্রতিটি boolean সম্ভাব্য অবস্থার সংখ্যা দ্বিগুণ করে। ৩টিতে ৮, ৫টিতে ৩২। অথচ বৈধ অবস্থা বাড়ে ধীরে। বাকিগুলো “অসম্ভব” হওয়ার কথা, কিন্তু কোডে সেগুলোতে পৌঁছানো সম্ভব। যেমন আবার চেষ্টা করার সময়ে কেউ setIsLoading(true) করলেন, setIsError(false) ভুলে গেলেন: পর্দায় একসাথে spinner আর error. কোনো test না থাকলে এটি ব্যবহারকারীই প্রথম দেখবেন। ধীর নেটওয়ার্কে এমন মিশ্রণ আরও সহজে দেখা যায়, কারণ দুটি আপডেটের মাঝের সময়টুকু তখন লম্বা হতে পারে।
২০১৬ সালের elm-conf-এ Richard Feldman-এর “Making Impossible States Impossible” বক্তৃতা এই চিন্তাকে পরিচিত করে: data model এমনভাবে বানান, যাতে অবৈধ মিশ্রণ লেখাই না যায়। TypeScript-এ পর্ব ০৬-এর discriminated union এই কাজ করে: { status: 'error', message: string }-এর মতো রূপে message শুধু error অবস্থায় থাকে। ফলে সফল অবস্থায় ভুল করে error বার্তা পড়লে compiler আটকে দেয়। আরও পুরোনো শিকড় আছে। David Harel ১৯৮৭ সালে statechart নিয়ে লিখেছিলেন, আর state machine-এর ধারণা কম্পিউটার বিজ্ঞানে তারও বহু আগের। UI-তে এটি মানে: কোন অবস্থা থেকে কোন অবস্থায় যাওয়া যায়, সেটিও স্পষ্ট করে লেখা।
দাবিটি অবশ্য নিরঙ্কুশ নয়। অনেক data লাইব্রেরি (পর্ব ১৯-এর TanStack Query) isFetching, isError-এর মতো boolean দেয়, কারণ background refetch-এর সময়ে পুরোনো data দেখানো আর নতুন করে আনা একসাথে বৈধ। অর্থাৎ সত্যিই একসাথে ঘটতে পারে এমন দুটি বিষয় হলে দুটি আলাদা মানই ঠিক। সেখানে লাইব্রেরি নিজেই মিশ্রণগুলো সামলায়, আপনি হাতে মেলান না।
মূল উপলব্ধি: প্রশ্ন “boolean না string” নয়, প্রশ্ন “এই অবস্থাগুলো কি একে অপরকে বাদ দেয়?” বাদ দিলে এক মান, একসাথে সম্ভব হলে আলাদা মান। ডেটার আকৃতি ঠিক হলে অনেক if নিজে থেকেই মুছে যায়।
প্র ০৫Pagination, infinite scroll না virtualization: বাজারঘরের ১০ হাজার পণ্যের পাতায় কোনটি নেবেন?
তিনটি আলাদা সমস্যার সমাধান, তাই আগে সমস্যা চিহ্নিত করুন। Pagination কমায় নেটওয়ার্কে আসা data আর DOM, দুটিই। প্রতি পাতার লিংক থাকে, শেয়ার করা যায়, সার্চ ইঞ্জিন সহজে পড়ে, ফিরে এলে আগের জায়গা পাওয়া যায়। Infinite scroll কমায় ক্লিকের ঘর্ষণ, কিন্তু নিজে থেকে DOM কমায় না, বরং সারি জমতেই থাকে। Virtualization কমায় শুধু DOM; data আগে থেকেই থাকতে হয়, অথবা infinite loading-এর সাথে জুড়তে হয়।
Infinite scroll-এর ইতিহাসও ভাবার মতো। Aza Raskin নিজে বলেছেন তিনি ২০০৬ সালের দিকে এই ধারণা নিয়ে কাজ করেন, পরে এর ফলে সময় নষ্টের প্রভাব নিয়ে আফসোস প্রকাশ করেছেন। কেনাকাটার পাতায় আরও বাস্তব সমস্যা আছে: ফুটারে পৌঁছানো যায় না, পণ্য দেখে ফিরে এলে জায়গা হারায়, আর বাজেট ফোনে সারি জমতে জমতে পাতা ভারী হয়।
বাজারঘরের ক্ষেত্রে আমার ভাবনা: পণ্যের তালিকায় sort আর filter সার্ভারে, URL-এ পাতা নম্বর, এক পাতায় ধরুন ২৪টি পণ্য। সার্চ ইঞ্জিন আর শেয়ারের জন্য এটি স্বাভাবিক। Virtualization সেখানে লাগে না। Admin-এর মজুত টেবিলে, যেখানে কর্মী হাজার সারিতে দ্রুত চোখ বোলান, সেখানে virtualization মানানসই। তখন Ctrl+F হারানোর ক্ষতি পুষিয়ে দিতে নিজস্ব search দিতে হবে।
CSS-এর content-visibility: auto আরেকটি হালকা পথ (২০২০ সালের দিকে Chrome 85-এ আসে; web.dev-এ এর পরিচিতি আছে)। পর্দার বাইরের অংশের layout আর paint এড়ায়, কিন্তু DOM-এ সব থাকে, তাই ব্রাউজারের খোঁজ আর accessibility tree অক্ষত থাকে। অবশ্য React-এর render খরচ এতে কমে না।
মূল উপলব্ধি: “কোন লাইব্রেরি” প্রশ্নের আগে জানতে হয় কোন সম্পদের অভাব: নেটওয়ার্ক, DOM, না ব্যবহারকারীর মনোযোগ। Pagination, infinite scroll আর virtualization একে অপরের বিকল্প নয়, ভিন্ন খরচের উত্তর। সিদ্ধান্ত নিন ব্যবহারকারীর কাজ দেখে, নিজের ল্যাপটপ দেখে নয়।
অনুশীলন
ওপরের live block-গুলো বদলে চালিয়েও এগুলো পরীক্ষা করতে পারেন।
-
Virtualization-এর হিসাব: “বাজারঘর”-এর admin টেবিলে ৫০,০০০ সারি। প্রতিটির উচ্চতা ৩২ px, দেখার জায়গা ৪৮০ px, overscan ওপরে-নিচে ৪টি করে, এক সারিতে ৬টি DOM element. (ক) ভেতরের ফাঁকা ঘরের মোট উচ্চতা? (খ) কতটি সারি আঁকা হবে? (গ) DOM element কত কমল? (ঘ) ৩,২০,০০০ px scroll করলে কোন সারি থেকে কোন সারি আঁকা হবে (০ থেকে গণনা)?
(ক) ৫০,০০০ × ৩২ = ১৬,০০,০০০ px. (খ) দৃশ্যমান ৪৮০ ÷ ৩২ = ১৫; মোট ১৫ + ৪ + ৪ = ২৩টি। (গ) সব আঁকলে ৫০,০০০ × ৬ = ৩,০০,০০০; এখন ২৩ × ৬ = ১৩৮. প্রায় ২,১৭৪ গুণ কম (৩,০০,০০০ ÷ ১৩৮ ≈ ২,১৭৩.৯)। (ঘ) প্রথম দৃশ্যমান ৩,২০,০০০ ÷ ৩২ = ১০,০০০. শেষ দৃশ্যমান ১০,০১৪. Overscan-সহ ৯,৯৯৬ থেকে ১০,০১৮, মোট ২৩টি।
-
আউটপুট অনুমান করুন: পর্দায় কী দেখা যাবে, সারি ধরে বলুন।
const a = 0; const b = ''; const c = [];, তারপর JSX:<p>[{a && 'A'}]</p>,<p>[{b && 'B'}]</p>,<p>[{c.length > 0 && 'C'}]</p>,<p>[{c && 'D'}]</p>.আসল run-এ চারটি সারি এলো:
[0],[],[],[D]. প্রথমটিতে0falsy, মান0, আর সংখ্যা আঁকা হয়। দ্বিতীয়টিতে ফাঁকা string, কিছু দেখা যায় না। তৃতীয়টিতেfalse, কিছু আঁকা হয় না। চতুর্থটি সবচেয়ে চমকের: খালি array JavaScript-এ truthy, তাই ‘D’ দেখা যায়। “Array খালি কি না” পরীক্ষায়.length > 0লিখতে হয়। -
নকশা ও সমালোচনা: একজন সহকর্মীর কোড:
const [items, setItems] = useState(data);আরuseEffect(() => setItems(data.filter(p => p.name.includes(q))), [q]);, তারপর{items.map((p, i) => <Row key={i} p={p} />)}, উপরে{loading && <Spinner />}{error && <Err />}. অন্তত পাঁচটি সমস্যা বলুন, আর কীভাবে সারাবেন।(১)
itemsderived value, state-এ অকারণ; render-এ হিসাব করুন। (২) Effect-এdatadependency নেই, তাই data বদলালে পুরোনো ফল থাকবে। (৩) Effect-এর জন্য প্রতি অক্ষরে এক বাড়তি render. (৪)key={i}: filter করলেই সারি সরে,p.idদিন। (৫) দুটি boolean-এ অসম্ভব মিশ্রণ সম্ভব; একটিstatusনিন। (৬) খালি ফলের কোনো অবস্থা নেই। (৭) ফলের সংখ্যা screen reader জানে না;role="status"দিন। (৮) ইংরেজি বড়-ছোট হাতের অক্ষর আর বাংলা normalize সামলানো নেই। -
Live block বদলে দেখুন: অংশ ৩-এর ব্লকে
key={i}-এর জায়গায়key={Math.random()}লিখুন। Input-এ কিছু লিখে “তালিকা উল্টান” চাপুন। তারপর অংশ ৫-এর ব্লকে[...rows].sortথেকে কপি সরিয়েrows.sortলিখে চালান। কী দেখলেন, কী বুঝলেন?প্রথম পরিবর্তনে ওপরের list-এর লেখা মুছে প্রতিটি input আবার শুরুর
defaultValue-তে ফেরে: সব DOM node নতুন তৈরি হয়েছে। লেবেল আর পরিমাণ মিললেও ব্যবহারকারীর নিজের লেখা হারিয়েছে।দ্বিতীয় পরিবর্তনে এই ছোট ব্লকে চোখে হয়তো কোনো তফাত দেখবেন না, কারণ প্রতি render-এ সাজানো হয়ে যায়। মজুতে সমান সারির ক্রম কিন্তু আগের সাজানোর ওপর নির্ভর করবে, মূল ক্রমের ওপর নয়। আর
rowsযদি state বা props হতো, তাহলে অন্য component-ও বদলে যাওয়া ক্রম দেখত। “চোখে ঠিক দেখাচ্ছে” আর “ঠিক আছে” এক কথা নয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি filter করা পণ্যের list বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- পণ্যের data-য় প্রতিটির স্থায়ী id আছে কি না নিশ্চিত করা
- খোঁজার লেখা আর category-কে state হিসেবে রাখা
- Render-এর সময়ে
filterদিয়ে দৃশ্যমান list হিসাব করা mapদিয়ে সারি আঁকা, key হিসেবে id- খালি ফলের বার্তা আর ফলের সংখ্যার ঘোষণা যোগ করা
React-এ list-এর key-এর মূল কাজ কী?
- সারির DOM element-এ CSS class যোগ করা
- কোন সারি কোন জিনিস, সেটি দুই render-এর মধ্যে React-কে চেনানো
- Component-কে তার id prop হিসেবে পৌঁছে দেওয়া
- List-কে key-এর মান অনুযায়ী স্বয়ংক্রিয়ভাবে সাজানো
তিনটি পণ্যের সারিতে uncontrolled input আছে, আর key হিসেবে array-র index দেওয়া। ব্যবহারকারী প্রথম সারির input-এ লিখলেন, তারপর list উল্টে গেল। কী ঘটবে?
- React error দিয়ে পাতা বন্ধ করবে
- Input-এর লেখা পণ্যের সাথে শেষ সারিতে চলে যাবে
- লেবেল বদলাবে, কিন্তু লেখা প্রথম সারিতেই থাকবে, ফলে অন্য পণ্যের পাশে দেখাবে
- সব input-এর লেখা মুছে যাবে
const count = 0; থাকলে <div>{count && <b>নতুন</b>}</div> পর্দায় কী আঁকবে?
- একটি 0
- কিছুই না
- “নতুন” লেখা
- false লেখা
একটি পণ্যের list আর একটি search লেখা আছে। দৃশ্যমান list রাখার সবচেয়ে ভালো পদ্ধতি কোনটি?
useEffect-এfilterচালিয়ে আলাদা state-এ রাখা- Filter-এর ফল দিয়ে মূল list-এর state প্রতিস্থাপন করা
- একটি global variable-এ রেখে দেওয়া
- Render-এর সময়ে হিসাব করা, state-এ না রাখা
isLoading, isError, isEmpty: তিনটি boolean. নিয়ম হলো এক সময়ে বড়জোর একটি true হবে। কয়টি মিশ্রণ সম্ভব, আর কয়টি অবৈধ?
- ৩টি সম্ভব, ০টি অবৈধ
- ৬টি সম্ভব, ২টি অবৈধ
- ৮টি সম্ভব, ৪টি অবৈধ
- ৮টি সম্ভব, ৭টি অবৈধ
একই জায়গায় থাকা <ReviewForm key={product.id} />-এ product.id বদলালে কী হয়?
- State থাকে, শুধু নতুন props আসে
- পুরোনো component-এর state আর DOM মুছে নতুন করে তৈরি হয়
- Error হয়, কারণ key শুধু list-এ বসে
- পাতা reload হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- List আঁকে
map; প্রতিটি সারিতে ভাইবোনদের মধ্যে অনন্য, স্থায়ীkey. - Index key শুধু স্থির, stateless list-এ; অন্য সব জায়গায় data-র id.
Math.random()কখনো নয়। - Filter, sort আর group derived value: render-এ হিসাব, state-এ কপি নয়।
- পুরো component বদলালে early return, দুই রূপে ternary, থাকা-না-থাকায় boolean-সহ
&&. - প্রতিটি list-এ loading, empty, error ভাবুন, একটি
status-এ;keyবদলে reset; লম্বা list-এ আগে মাপুন; ফলের সংখ্যা আর focus-এর যত্ন নিন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১২ · Form — controlled input, submit আর ব্যবহারকারীর ইনপুটপরবর্তী পর্বInput-এর মান React-এর state-এ আনা।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- JavaScript array methodভিত্তি
map,filter,sort-এর নিয়ম। - HTML list আর tableভিত্তিকোন data-য় কোন element.
- HTML accessibilityগভীরে যানSemantic HTML আর screen reader.
- Broken access controlপাশের ট্র্যাকUI লুকালেই data নিরাপদ হয় না কেন।