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

Props আর composition — component জোড়া দেওয়া

Props and Composition — Putting React Components Together

Taught in Bangla, this lesson treats props as a component's read-only input: destructuring, defaults, rest and spread, typing the props contract in TypeScript, children and slots, why piling up boolean flags explodes states and how variants and composition help, callbacks that send events upward, container splits, render props and compound components, an accessible Button and ProductCard for Bazarghar, and the prop-drilling problem the next module fixes.

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

  • Props কেন read-only, আর ভাঙলে development ও production-এ আসলে কী ঘটে
  • Destructuring, default, rest, spread, আর spread কখন ক্ষতি করে
  • TypeScript দিয়ে props-এর চুক্তি লেখা, children আর slot prop
  • Composition বনাম configuration, callback, container ভাগ, render prop আর compound component
  • “বাজারঘর”-এর জন্য প্রবেশযোগ্য Button আর ProductCard নকশা, আর prop drilling চেনা

১ · Props — রান্নাঘরে পাঠানো অর্ডারের স্লিপ

একটি রেস্তোরাঁ ভাবুন। ওয়েটার টেবিল থেকে অর্ডার নিয়ে রান্নাঘরে একটি স্লিপ পাঠালেন: “২ প্লেট কাচ্চি, ঝাল কম।” বাবুর্চি স্লিপ পড়েন, স্লিপ অনুযায়ী রান্না করেন। তিনি স্লিপে কলম চালান না। ক্রেতা মত বদলালে ওয়েটার নতুন স্লিপ আনেন।

React-এ component হলো বাবুর্চি, আর props হলো সেই স্লিপ। পর্ব ০৭-এ দেখেছেন component একটি function. পর্ব ০৮-এ দেখেছেন JSX আসলে function ডাকার সংক্ষিপ্ত রূপ। তাই <PriceTag taka={140} unit="কেজি" /> লিখলে React এই object-টি বানিয়ে function-কে দেয়:

PriceTag({ taka: 140, unit: "কেজি" })

লক্ষ করুন, taka={140}-তে বাঁকা বন্ধনী আছে, কারণ এটি JavaScript-এর সংখ্যা। unit="কেজি"-তে উদ্ধৃতি আছে, কারণ এটি সাধারণ string. Props-এ যেকোনো মান যায়: সংখ্যা, লেখা, array, object, function, এমনকি অন্য JSX.

মূল ধারণা

Component-এর দৃষ্টিতে props হলো read-only ইনপুট। একই props দিলে একই UI আসবে, এটিই React-এর প্রত্যাশা। প্রতিবার render-এ component props-এর একটি নতুন মুহূর্তচিত্র পায়। পুরোনোটি বদলানো হয় না, বদলে যায় পুরো স্লিপ।

নিয়মটি ভাঙলে কী হয়? একটি ছোট project-এ component-টি নিজের prop দ্বিগুণ করার চেষ্টা করল। একই কোড React-এর দুই build-এ চালানো হলো।

function Price(props: { taka: number }) {
  props.taka = props.taka * 2;   // নিয়ম ভাঙা: prop বদলানো
  return <span>{props.taka} টাকা</span>;
}

console.log(renderToString(<Price taka={140} />));

সত্যিকারের run · লেখার সময়ে React 19.3.1, tsx 4.23 · ~/bazarghar

$ NODE_ENV=development npx tsx src/mutate.tsx
TypeError: Cannot assign to read only property 'taka' of object '#<Object>'
    at Price (src/mutate.tsx:4:9)
    …

$ NODE_ENV=production npx tsx src/mutate.tsx
<span>280<!-- --> টাকা</span>

ফলটি শিক্ষণীয়। Development build-এ React props object-কে জমিয়ে (Object.freeze) দেয়, তাই সঙ্গে সঙ্গে error আসে। Production build-এ বাড়তি খরচ এড়াতে এই পাহারা থাকে না। সেখানে কোনো error নেই, শুধু ১৪০ টাকার ডাল হয়ে যায় ২৮০ টাকা। নিয়মটি production-এ পাহারাহীন, তাই ভুলটি ধরার জায়গা development.

এই পর্বের live block-গুলো React-এর production build-এ চলে। তাই সেখানে prop বদলালে অনেক সময় error দেখবেন না। Error না আসা মানে কোড ঠিক, এমন ভাববেন না।

এবার একটি ট্রি ধরে props-এর চলাচল দেখুন। “বাজারঘর”-এর পণ্যের পাতায় ডেটা সবচেয়ে ওপরের component-এ আছে। “পরে” চাপুন, আর প্রতিটি ধাপে props object-টি খেয়াল করুন।

  1. ProductPageproductsডেটার মালিক
  2. ProductGridproductsতালিকা সাজায়
  3. ProductCardproductএকটি পণ্য
  4. Buttonchildrenএকটি বোতাম
ProductPageডেটা এখানেই থাকে
const products = [
  { id: 7, name: "মসুর ডাল", price: 140 },
  { id: 9, name: "মিনিকেট চাল", price: 85 },
];
ProductGrid-এর propsপুরো তালিকা নামল
{ products: [ …২টি পণ্য… ] }
ProductCard-এর propsকার্ডপ্রতি আলাদা object
প্রথম কার্ড:  { product: { id: 7, name: "মসুর ডাল", price: 140 } }
দ্বিতীয় কার্ড: { product: { id: 9, name: "মিনিকেট চাল", price: 85 } }
Button-এর propsযতটুকু দরকার, ততটুকুই
{ variant: "primary", children: "কার্টে যোগ করুন" }
পণ্যের দাম বা নাম Button জানে না, জানার দরকারও নেই
দাম বদলালনতুন স্লিপ, পুরোনোতে কলম নয়
আগের প্রথম কার্ড: { product: { id: 7, …, price: 140 } }
পরের প্রথম কার্ড: { product: { id: 7, …, price: 150 } }
  1. ধাপ ১ — ডেটার মালিক। ProductPage-এর কাছে ২টি পণ্য আছে: ডাল ১৪০ টাকা, চাল ৮৫ টাকা। নিচের কেউ এই ডেটা এখনো দেখেনি।
  2. ধাপ ২ — তালিকা নামল। ProductGrid props হিসেবে পুরো array পেল। সে নিজে দাম জানতে চায় না, শুধু তালিকা সাজায়।
  3. ধাপ ৩ — একটি করে পণ্য। প্রতিটি পণ্যের জন্য একটি ProductCard. দুই কার্ড পেল দুটি আলাদা props object. Component এক, ইনপুট আলাদা, তাই পর্দার ফল আলাদা।
  4. ধাপ ৪ — ক্ষুদ্রতম টুকরো। Button পেল শুধু variant আর লেখা। যে component যত কম জানে, তাকে তত বেশি জায়গায় ব্যবহার করা যায়।
  5. ধাপ ৫ — বদল মানে নতুন object. ডালের দাম ১৫০ হলে ওপর থেকে নতুন props নামে। প্রথম কার্ড পায় price: 150. কোনো নিচের component ১৪০ লেখা object-টিতে হাত দেয়নি।

২ · Destructuring, default, rest আর spread

এই চারটি JavaScript-এরই কৌশল, React-এর নয়। ভাষার দিকটি আছে JavaScript কোর্সের destructuring আর spread পর্বে. এখানে শুধু props-এ এদের ব্যবহার দেখব। Destructuring মানে object-এর ঘরগুলো আলাদা নামে খুলে নেওয়া। Component-এর প্রথম লাইনেই এটি করলে বোঝা যায় সে কোন কোন prop নেয়।

function PriceTag({ taka, unit = "কেজি", ...rest }) {
  return <span {...rest}>৳{taka} / {unit}</span>;
}

<PriceTag taka={140} />                       // unit হবে "কেজি"
<PriceTag taka={95} unit="লিটার" title="তেল" /> // title যাবে rest-এ
কৌশললেখাকাজ
Destructuring{ taka, unit }ঘর খুলে নাম দেওয়া
Defaultunit = "কেজি"prop না এলে বিকল্প মান
Rest...restবাকি সব ঘর এক object-এ জড়ো করা
Spread{...rest}object-এর সব ঘর prop হিসেবে ছড়িয়ে দেওয়া

Default নিয়ে একটি সূক্ষ্ম ফাঁদ আছে। Default মান বসে শুধু prop অনুপস্থিত হলে, অথবা মান undefined হলে। size={null} বা size={0} দিলে default বসে না। API থেকে null এলে তাই default আপনাকে বাঁচাবে না।

Spread-এ ক্রম গুরুত্বপূর্ণ, কারণ পরের লেখা আগেরটিকে ঢেকে দেয়। <button type="button" {...rest}> মানে ব্যবহারকারী চাইলে type বদলাতে পারবেন। <button {...rest} type="button"> মানে পারবেন না। বাস্তব কাজে দুই রকমই দরকার হয়, তাই ক্রম ভেবে লিখতে হয়।

Spread কখন ক্ষতি করে

React-এর docs-ও বলে, spread সংযমের সঙ্গে ব্যবহার করতে হবে। পর্ব ০৬-এ দেখেছিলেন, costPrice মানে কেনা দাম, ক্রেতার কাছে যাওয়ার কথা নয়। এবার একটি অলস component পুরো পণ্যটি DOM-এ ছড়িয়ে দিল।

const product = { id: "p7", name: "মসুর ডাল", price: 140, costPrice: 110 };

function Row(props: typeof product) {
  return <li {...props}>{props.name}</li>;
}

সত্যিকারের run · React 19.3.1 · development আর production, দুই build-এ একই HTML

$ NODE_ENV=development npx tsx src/spread.tsx
React does not recognize the `costPrice` prop on a DOM element. …
<li id="p7" name="মসুর ডাল" price="140" costPrice="110">মসুর ডাল</li>

$ npx tsc
(কোনো error নেই)

তিনটি জিনিস একসাথে ঘটেছে। কেনা দাম HTML-এ চলে গেছে, যে কেউ “View Source” করলে দেখবে। React শুধু development-এ একটি সতর্কবার্তা দিয়েছে। আর সবচেয়ে অস্বস্তিকর, tsc চুপ। JSX-এ spread করলে TypeScript বাড়তি ঘর খোঁজে না। Type শুধু দেখে দরকারি ঘরগুলোর মান ঠিক আছে কি না। আরেকটি বিপদ আরও গুরুতর:

// ধরুন এই JSON বাইরের API থেকে এসেছে
const fromApi = JSON.parse(
  '{"title":"অফার","dangerouslySetInnerHTML":{"__html":"<b>injected</b>"}}'
);
function Banner(props: Record<string, unknown>) {
  return <div {...props} />;
}

সত্যিকারের run · React 19.3.1

$ npx tsx src/spread2.tsx
<div title="অফার"><b>injected</b></div>

বাইরের JSON নিজেই একটি prop-এর নাম ঠিক করে দিল, আর অচেনা HTML পাতায় ঢুকে গেল। এটিই XSS (পাতায় অন্যের ক্ষতিকর script ঢোকানো)-এর দরজা, যার তত্ত্ব আছে Cybersecurity কোর্সের XSS পর্বে. নিয়ম: যে object-এর ঘরগুলো আপনি নিজে জানেন না, তা কখনো DOM element-এ spread করবেন না।

Spread নিরাপদ

পাতলা wrapper component-এ, যেখানে rest-এ শুধু DOM-এর স্বীকৃত attribute থাকে। যেমন Button, যা <button>-এর props-ই নেয়।

Spread ক্ষতিকর

API response, database row বা অজানা object-এ। সব component-এ অভ্যাসবশত {...props} লিখলে কে কী পাচ্ছে, তা আর কোড পড়ে বোঝা যায় না।

আরও গভীরে: JSX compile হয়ে একটি সাধারণ object হয়, যাতে type, key, props আর $$typeof ঘর থাকে। $$typeof-এর মান একটি Symbol. JSON-এ Symbol রাখা যায় না, তাই API থেকে আসা JSON নিজেকে React element সাজাতে পারে না। কিন্তু এই পাহারা dangerouslySetInnerHTML-এর মতো সাধারণ prop-কে আটকায় না, যা ওপরের run দেখাল। প্রতিরক্ষা তাই spread না করা।

৩ · Props-এর চুক্তি — TypeScript দিয়ে লেখা

একটি component আসলে একটি চুক্তি: “আমাকে এগুলো দিলে আমি এই UI দেব।” পর্ব ০৬-এ AddToCart-এর type এক ঝলক দেখেছিলেন। এবার “বাজারঘর”-এর আসল Button লিখি।

import type { ComponentProps, ReactNode } from "react";

type ButtonProps = ComponentProps<"button"> & {
  variant?: "primary" | "secondary" | "danger";
  size?: "sm" | "md";
  loading?: boolean;
  children: ReactNode;
};

প্রতিটি অংশের একটি কাজ আছে। ComponentProps<"button"> মানে HTML button যত attribute নেয়, সব: disabled, onClick, aria-label… হাতে তালিকা লিখতে হয় না। ? মানে ঘরটি ঐচ্ছিক. Literal মানের union বলে দেয় শুধু এই তিনটি variant চলবে। ReactNode হলো যা কিছু React দেখাতে পারে: লেখা, সংখ্যা, JSX, null, অথবা এদের array.

এবার ইচ্ছা করে পাঁচটি ভুল করা হলো। প্রতিটি ভুল সাধারণ জীবনে ঘটে।

<Button variant="dangerous">মুছুন</Button>                  // ৬: বানান ভুল
<ProductCard product={dal} />                              // ৭: onAdd নেই
<ProductCard product={dal} onAdd={(id: string) => {}} />    // ৮: ভুল type
<Button />                                                 // ৯: children নেই

function Tag(props: Readonly<{ text: string }>) {
  props.text = "নতুন";                                     // ১২: prop বদলানো
  …
}

সত্যিকারের run · TypeScript 7.0.2, @types/react 19.3.0 · লম্বা বার্তা ছোট করা হয়েছে

$ npx tsc
src/page.tsx(6,26): error TS2820: Type '"dangerous"' is not assignable to type '"danger" | "primary" | "secondary" | undefined'. Did you mean '"danger"'?
src/page.tsx(7,19): error TS2741: Property 'onAdd' is missing in type '{ product: Product; }' but required in type 'ProductCardProps'.
src/page.tsx(8,45): error TS2322: Type '(id: string) => void' is not assignable to type '(id: number) => void'.
  Types of parameters 'id' and 'id' are incompatible.
    Type 'number' is not assignable to type 'string'.
src/page.tsx(9,19): error TS2741: Property 'children' is missing in type '{}' but required in type …
src/page.tsx(12,9): error TS2540: Cannot assign to 'text' because it is a read-only property.

প্রথম লাইনে compiler বানানও শুধরে দিতে চাইছে: Did you mean '"danger"'? শেষ লাইনটি লক্ষ করুন। Readonly<…> লিখলে prop বদলানোর ভুল compile-এর সময়ই ধরা পড়ে। ওপরের mutate.tsx-এ Readonly ছিল না, তাই tsc সেখানে কিছু বলেনি। সেই ভুল ধরেছিল শুধু development-এর চলার সময়।

Type চলার সময় মুছে যায়, যা type erasure নামে পর্ব ০৬-এ দেখেছেন। product: Product লিখলেই API-র উত্তর সত্যি Product হয়ে যায় না। Props-এর type আপনার কোডের ভেতরের ভুল ধরে। বাইরের ডেটা আগে validation পার করতে হয়।

৪ · children আর slot — component-এর ফাঁকা গর্ত

মিষ্টির দোকানের বাক্স ভাবুন। বাক্সের আকার, ঢাকনা, দোকানের ছাপ সব ঠিক করা। ভেতরে রসগোল্লা থাকবে না সন্দেশ, তা বাক্স জানে না। Children prop ঠিক এই ভেতরের জায়গা। Opening আর closing tag-এর মাঝে যা লেখা হয়, React তা children নামে props-এ দেয়।

আর বাক্সের গায়ে যদি আলাদা একটি পকেট থাকে? সেটি slot prop: একটি নামধারী prop, যার মান JSX. নিচের Card-এ children হলো মূল জায়গা, আর footer হলো একটি slot. কোডটি সম্পাদনা করেও দেখতে পারেন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Card({ title, footer, children }) {
  console.log('Card:', title, '| footer:', Boolean(footer), '| children:', children.type);
  return (
    <section style={{ border: '1px solid #94a3b8', borderRadius: 10, padding: 12, margin: 8 }}>
      <h3 style={{ margin: 0 }}>{title}</h3>
      {children}
      {footer && <footer style={{ color: '#475569' }}>{footer}</footer>}
    </section>
  );
}
function App() {
  return (
    <>
      <Card title="মসুর ডাল" footer={<small>স্টকে আছে ১২টি</small>}>
        <p>দাম: ৳140, প্রতি কেজি</p>
      </Card>
      <Card title="আজকের অফার">
        <ul><li>৫ কেজি চালে ডেলিভারি ফ্রি</li><li>তেলে ১০% ছাড়</li></ul>
      </Card>
    </>
  );
}
createRoot(document.getElementById('root')).render(<App />);
</script>

আউটপুটের দুই লাইন: Card: মসুর ডাল | footer: true | children: p আর Card: আজকের অফার | footer: false | children: ul. একই Card, অথচ প্রথমবার ভেতরে p, দ্বিতীয়বার ul. দ্বিতীয় কার্ডে footer না থাকায় footer && … কিছুই দেখায়নি। Card-এর কোডে “ডাল” বা “অফার” কোথাও নেই। সে শুধু কাঠামো জানে। children.type ছাপা হয়েছে, কারণ children-ও একটি সাধারণ React element. তবে বাস্তব কোডে ভেতরে ঢুকে এটি ঘাঁটাবেন না, শুধু বসিয়ে দেবেন।

Slot prop-এ শুধু লেখা নয়, পুরো JSX যায়। তাই footer-এ কাল একটি লিংক বা বোতাম দিলে Card বদলাতে হবে না। React-এর পুরোনো docs-এ SplitPane উদাহরণে left আর right নামের দুটি prop দেখানো হয়েছিল, আর বলা হয়েছিল এগুলো অন্য library-র “slots”-এর কথা মনে করাতে পারে। Vue আর Web Components-এ <slot> নামে আলাদা ব্যবস্থাই আছে। React-এ আলাদা কিছু লাগে না, কারণ JSX নিজেই একটি মান।

৫ · Composition বনাম configuration — boolean-এর বিস্ফোরণ

দুইভাবে একটি component-কে নমনীয় করা যায়। Configuration মানে একটি component-এ নতুন নতুন prop যোগ করা, যেন একটি যন্ত্রে সুইচ বাড়ছে। Composition মানে ছোট ছোট component জোড়া দিয়ে নতুন জিনিস গড়া, যেন দর্জি কলার, হাতা আর বোতাম জুড়ে জামা বানান।

শুরুতে দুটিই ঠিক লাগে। সমস্যা শুরু হয় সময়ের সঙ্গে। প্রতিটি নতুন চাহিদায় একটি showX বা isY যোগ হয়। একে বলে boolean prop explosion. হিসাবটি সরল:

n-টি স্বাধীন boolean → 2n রকম অবস্থা
৪টি → ১৬ · ৫টি → ৩২ · ৭টি → ১২৮

ধরুন Button-এ primary, secondary, danger আর small flag আছে। ৪টি flag মানে 24 = ১৬ রকম। অথচ অর্থপূর্ণ অবস্থা কয়টি? রং একটিই হতে পারে, আর আকার ২ রকম: ৩ × ২ = ৬. বাকি ১৬ − ৬ = ১০টি অবস্থা অর্থহীন, যেমন একসাথে primary আর danger. এখন variant আর size দিলে সম্ভাব্য অবস্থা ঠিক ৩ × ২ = ৬টি। অর্থহীন অবস্থা লেখাই যায় না। নিচের block-এ দুটি পাশাপাশি দেখুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const COLORS = { primary: '#2563eb', secondary: '#475569', danger: '#dc2626' };
const look = (bg) => ({ background: bg, color: '#fff', border: 0, borderRadius: 8, padding: '8px 14px', margin: 4 });

function FlagButton({ primary, danger, children }) {
  const bg = primary ? COLORS.primary : danger ? COLORS.danger : COLORS.secondary;
  console.log('FlagButton', { primary, danger }, '=>', bg);
  return <button type="button" style={look(bg)}>{children}</button>;
}
function Button({ variant = 'secondary', children }) {
  const bg = COLORS[variant];
  console.log('Button', variant, '=>', bg);
  return <button type="button" style={look(bg)}>{children}</button>;
}
function App() {
  return (
    <>
      <FlagButton primary danger>মুছুন (flag)</FlagButton>
      <Button variant="danger">মুছুন (variant)</Button>
      <Button>বাতিল</Button>
    </>
  );
}
createRoot(document.getElementById('root')).render(<App />);
</script>

আউটপুটে FlagButton { primary: true, danger: true } => #2563eb: দুটি flag একসাথে দিলে কোডের ভেতরের if-এর ক্রম নীরবে একটিকে জিতিয়ে দিয়েছে। “মুছুন” বোতামটি হয়ে গেছে নীল! কোনো error নেই। পরের দুই লাইন Button danger => #dc2626 আর Button secondary => #475569. এখানে একটি prop, একটি মান, দ্বন্দ্বের কোনো সুযোগ নেই। আর TypeScript থাকলে ভুল variant-এর নাম compile-এর সময়ই ধরা পড়ে।

Variant শুধু “কোন একটি” ধরনের পছন্দ সামলায়। কিন্তু পণ্যের কার্ডে যখন এক পাতায় রেটিং লাগে, আরেক পাতায় লাগে না, তখন সমাধান composition. নিচের ধাপগুলো একটি বাস্তব পরিণতি দেখায়।

শুরুflag: ০টি · অবস্থা: ১
<ProductCard product={p} />
প্রথম চাহিদাflag: ২টি · অবস্থা: ৪
<ProductCard product={p} showBadge showRating />
ছয় মাস পরেflag: ৫টি · অবস্থা: ৩২
<ProductCard product={p} showBadge showRating
  compact hideButton isWishlist />

// ভেতরে:
{showBadge && !compact && …}
{!hideButton && !isWishlist && …}
{isWishlist ? … : compact ? … : …}
ভাগ করা হলোছোট, একমুখী টুকরো
function Card({ children }) { … }        // কাঠামো
function ProductImage({ product }) { … }
function ProductPrice({ product }) { … }
function Rating({ value }) { … }
function AddButton({ onAdd }) { … }
প্রতিটি পাতা নিজে সাজায়flag: ০টি
// হোম পাতা
<Card>
  <ProductImage product={p} />
  <ProductPrice product={p} />
  <AddButton onAdd={add} />
</Card>
// wishlist পাতা
<Card>
  <ProductImage product={p} />
  <RemoveButton onRemove={remove} />
</Card>
  1. ধাপ ১ — সরল শুরু। একটি পণ্য, একটি কার্ড। কোনো flag নেই, তাই test করার অবস্থাও ১টি।
  2. ধাপ ২ — দুটি নিরীহ flag. ব্যাজ আর রেটিং ঐচ্ছিক হলো। ২টি flag মানে ২ × ২ = ৪ অবস্থা। এখনো ঠিক আছে।
  3. ধাপ ৩ — গিঁট। Wishlist আর ছোট কার্ড এলো। ৫টি flag মানে ৩২ অবস্থা। শর্তগুলো একে অন্যের ওপর নির্ভর করছে। compact আর isWishlist একসাথে দিলে কী হবে? কেউ জানে না, আর কেউ test-ও করেনি।
  4. ধাপ ৪ — টুকরো। প্রতিটি অংশ আলাদা component হলো। প্রতিটির কাজ একটি, আর ইনপুট অল্প। “Compact” এখন আলাদা কিছু নয়, কম টুকরো বসানো।
  5. ধাপ ৫ — জোড়া। কোন পাতায় কী দেখাবে, তা সেই পাতার JSX পড়েই বোঝা যায়। Card বদলাতে হয়নি, flag-ও নেই। দাম হলো পাতায় কয়েক লাইন বেশি JSX. বিনিময়ে শর্তের গিঁট চলে গেল।
দিকConfiguration (prop)Composition (জোড়া)
ভালো কখনপছন্দ সীমিত ও একই ধরনের: variant, sizeকাঠামো পাতাভেদে আলাদা
ব্যবহারের সুবিধাএক লাইনে শেষকয়েক লাইন, কিন্তু পরিষ্কার
একরূপতাসহজে বজায় থাকেনিয়ম না থাকলে পাতাভেদে ছড়িয়ে যায়
ঝুঁকিঅর্থহীন অবস্থা জমেঅতিরিক্ত টুকরো, অকারণ abstraction

এখানে কোনো চিরস্থায়ী বিজয়ী নেই। ভালো design system দুটিই ব্যবহার করে: ছোট টুকরোতে configuration, বড় কাঠামোতে composition. একটি কাজের সূত্র হলো, দুটি prop যদি একসাথে অর্থহীন হয়, তবে সেগুলো boolean হওয়া উচিত নয়। আর একটি component যদি “এটি হলে এই, ওটি হলে ওই” দিয়ে ভরে যায়, তবে সে আসলে দুটি component.

৬ · Callback — নিচ থেকে ওপরে খবর, আর re-render

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

Props নামে, callback ওঠে

Data flows down as props; events travel up through callbacks

Component ট্রিতে props নিচে নামে আর callback ওপরে ওঠে ProductPage ডেটা আর কার্টের মালিক ProductGrid ProductCard ProductCard Button Button products, onAdd onAdd(7) ক্লিক ━ props: ডেটা নিচে নামে ╌ callback: ঘটনার খবর ওপরে ওঠে
ছবি ৯.১ — ডেটার পথ আর খবরের পথ একই ট্রি ধরে, উল্টো দিকে।

নিচের কোডে useState আছে। পর্ব ১০-এ এটি পুরো শিখবেন। এখন শুধু জেনে রাখুন, এটি component-এর একটি স্মৃতি, আর setCount সেই স্মৃতি বদলের অনুরোধ জানায়। ধাপে ধাপে দেখুন একটি ক্লিক কোথা থেকে কোথায় যায়।

1function Shop() {2  const [count, setCount] = useState(0);3  const add = (id) => setCount(count + 1);4  return <Card id={7} count={count} onAdd={add} />;5}6function Card({ id, count, onAdd }) {7  return <button onClick={() => onAdd(id)}>কার্টে: {count}</button>;8}
মান (Watch)
Shop: count
Card: id
Card: count
Card: onAdd
  1. Shop render শুরু। count 0. Card এখনো কিছু পায়নি।
  2. Props তৈরি। Shop একটি function add বানাল, ধরা যাক তার নাম add#1. Card পেল { id: 7, count: 0, onAdd: add#1 }.
  3. Card render. পর্দায় বোতাম এলো: “কার্টে: 0”. Card জানে না onAdd কী করে, শুধু জানে কখন ডাকতে হবে।
  4. ক্লিক। ব্যবহারকারী বোতাম চাপলেন। Card নিজের props থেকে onAdd(7) ডাকল। খবরটি এখন ওপরে উঠছে।
  5. Shop-এর function চলল। add Shop-এর ভেতরে লেখা, তাই সে Shop-এর setCount ব্যবহার করতে পারে। অনুরোধ গেল: count হোক 0 + 1 = 1. এই মুহূর্তে count এখনো 0.
  6. Shop আবার render. এবার count 1. Shop একটি নতুন function add#2 বানাল।
  7. নতুন props নামল। Card পেল { id: 7, count: 1, onAdd: add#2 }. পর্দায় “কার্টে: 1”. Card একবারও নিজের props বদলায়নি।

“Props বদলালে re-render হয়” — কথাটি অর্ধেক সত্য

অনেকে শোনেন, props বদলালে component আবার চলে। ফলাফল দেখে এটি ঠিকই মনে হয়, কিন্তু কারণটি উল্টো। Props নিজে থেকে বদলায় না। বাবা re-render হলে তার সব সন্তান ডিফল্টভাবে আবার render হয়, props বদলাক বা না বদলাক। নতুন props সেই render-এর সঙ্গেই আসে। নিচের block শুরুতে একবার render হয়, তারপর নিজে থেকে প্রথম বোতামে একটি ক্লিক করে। আপনিও ক্লিক করতে পারেন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function ProductRow({ name, onAdd }) {
  console.log('  render ProductRow:', name);
  return <li>{name} <button type="button" onClick={() => onAdd(name)}>যোগ করুন</button></li>;
}
function ShopFooter() {
  console.log('  render ShopFooter (কোনো prop নেই)');
  return <small>বাজারঘর, ঢাকা</small>;
}
function Shop() {
  const [count, setCount] = useState(0);
  console.log('render Shop, count =', count);
  function handleAdd(name) {
    console.log('callback উঠে এলো:', name);
    setCount(count + 1);
  }
  return (
    <div>
      <p>কার্টে আছে: {count}টি</p>
      <ul><ProductRow name="ডাল" onAdd={handleAdd} /><ProductRow name="চাল" onAdd={handleAdd} /></ul>
      <ShopFooter />
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Shop />);
setTimeout(() => { console.log('--- একটি ক্লিক ---'); document.querySelector('button').click(); }, 100);
</script>

ক্লিকের পরের অংশটি খেয়াল করুন: callback উঠে এলো: ডাল, তারপর render Shop, count = 1, তারপর দুটি render ProductRow আর render ShopFooter (কোনো prop নেই). Footer-এর কোনো prop নেই, কিছুই বদলায়নি, তবু সে আবার চলেছে। মোট ৪টি function আবার ডাকা হয়েছে। তার মানে এই নয় যে পুরো DOM নতুন করে আঁকা হয়েছে। Render মানে শুধু function ডাকা। DOM-এ যায় শুধু পার্থক্যটুকু, এখানে “কার্টে আছে” লাইনের সংখ্যা। সেই হিসাব আসবে পর্ব ১৫-এ।

একটি হিসাব করা যাক। একটি ProductGrid-এ nটি ProductCard, আর প্রতিটি কার্ডে ২টি সন্তান, PriceTag আর Button. Grid একবার re-render হলে function ডাক হয়:

১ + n + 2n = 3n + 1
n = ২০ হলে ৩ × ২০ + ১ = ৬১টি function ডাক

সাধারণত এটি কোনো সমস্যা নয়, কারণ এই function-গুলো খুব দ্রুত চলে। মেপে সমস্যা না পেলে অকারণে সারানোর দরকার নেই। মাপা আর memo-র ব্যবহার আছে পর্ব ২৩-এ। সেখানে দেখবেন, add#1 আর add#2-এর মতো প্রতিবার নতুন function কেন গুরুত্বপূর্ণ।

৭ · Container ভাগ, render prop আর compound component

Container আর presentational

রেস্তোরাঁয় কেউ বাজার করেন, কেউ প্লেট সাজান। Container component ডেটা আনে আর সিদ্ধান্ত নেয়। Presentational component শুধু props পড়ে UI দেখায়। এই ভাগ পর্ব ০৪-এর separation of concerns-এরই ছোট রূপ।

// Container: ডেটা কোথা থেকে আসে, সে জানে
function ProductListPage() {
  const { products, loading } = useProducts();       // custom hook, পর্ব ১৪
  if (loading) return <p>লোড হচ্ছে…</p>;
  return <ProductGrid products={products} onAdd={addToCart} />;
}

// Presentational: শুধু props থেকে UI
function ProductGrid({ products, onAdd }: ProductGridProps) { /* … */ }

লাভ হলো, ProductGrid-কে নকল ডেটা দিয়ে সহজে test করা যায়, আর একই grid অন্য পাতায় ব্যবহার করা যায়। তবে এটি কঠিন আইন নয়। ২০১৫ সালের দিকে Dan Abramov এই ভাগ জনপ্রিয় করেন, আর Hooks আসার পরে তিনি নিজেই লেখেন, এখন আর এভাবে জোর করে ভাগ করার পরামর্শ দেন না। Logic এখন hook-এ সরানো যায়। তাই ভাবনাটি রাখুন, নিয়মটি নয়।

Render prop

কখনো component-এর হাতে ডেটা আছে, কিন্তু প্রতিটি ঘর দেখতে কেমন হবে তা ব্যবহারকারী ঠিক করবেন। তখন JSX-এর বদলে একটি function prop পাঠানো হয়। React-এর docs একে বলে render prop, মানে UI-এর একটি অংশ কীভাবে আঁকা হবে তা ঠিক করা prop.

function PagedList<T>({ items, pageSize, renderItem }: {
  items: T[];
  pageSize: number;
  renderItem: (item: T) => ReactNode;
}) {
  return <ul>{items.slice(0, pageSize).map(renderItem)}</ul>;
}

<PagedList items={products} pageSize={20}
  renderItem={(p) => <li key={p.id}>{p.name} — ৳{p.price}</li>} />

এখানে generic T থাকায় p-এর type হারায় না। আজকাল logic ভাগ করার বেশিরভাগ কাজ custom hook করে। তবে “ঘরটি কেমন দেখাবে” বলার জন্য renderItem-ধরনের prop এখনো খুব প্রচলিত, যেমন লম্বা তালিকা বা টেবিলের library-তে।

Compound component

HTML-এর <select> আর <option> ভাবুন। আলাদাভাবে এদের অর্থ নেই, একসাথে কাজ করে। Compound component মানে এমন কয়েকটি component, যারা গোপনে একটি state ভাগ করে। ব্যবহারকারী JSX-এ এদের ইচ্ছামতো সাজাতে পারেন। নিচের Tabs-এ createContext সেই গোপন পথ। Context পুরো শিখবেন পর্ব ১৭-এ। এখানে শুধু দেখুন, মাঝের কাউকে props দিতে হচ্ছে না।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const TabsCtx = createContext(null);
function Tabs({ defaultValue, children }) {
  const [active, setActive] = useState(defaultValue);
  return <TabsCtx value={{ active, setActive }}>{children}</TabsCtx>;
}
const TabList = ({ label, children }) => <div role="tablist" aria-label={label}>{children}</div>;
function Tab({ value, children }) {
  const { active, setActive } = useContext(TabsCtx);
  const on = active === value;
  return <button type="button" role="tab" id={'t-' + value} aria-selected={on} aria-controls={'p-' + value}
    tabIndex={on ? 0 : -1} style={{ fontWeight: on ? 700 : 400 }} onClick={() => setActive(value)}>{children}</button>;
}
function TabPanel({ value, children }) {
  const { active } = useContext(TabsCtx);
  return <div role="tabpanel" id={'p-' + value} aria-labelledby={'t-' + value} hidden={active !== value}>{children}</div>;
}
createRoot(document.getElementById('root')).render(
  <Tabs defaultValue="info">
    <TabList label="পণ্যের তথ্য"><Tab value="info">বিবরণ</Tab><Tab value="review">রিভিউ</Tab></TabList>
    <TabPanel value="info">মসুর ডাল, ১ কেজি প্যাকেট</TabPanel>
    <TabPanel value="review">৪.৫ তারা, ১২টি রিভিউ</TabPanel>
  </Tabs>);
const show = () => console.log('দেখা যাচ্ছে:', document.querySelector('[role=tabpanel]:not([hidden])').textContent);
setTimeout(() => { show(); document.getElementById('t-review').click(); setTimeout(show, 50); }, 100);
</script>

আউটপুটের দুই লাইন: দেখা যাচ্ছে: মসুর ডাল, ১ কেজি প্যাকেট, আর স্বয়ংক্রিয় ক্লিকের পরে দেখা যাচ্ছে: ৪.৫ তারা, ১২টি রিভিউ. Tab আর TabPanel একে অন্যকে কোনো prop দেয়নি, তবু একসাথে বদলেছে। কারণ দুজনেই একই context পড়ে। ব্যবহারকারী চাইলে TabList-কে নিচে নিতে বা মাঝে অন্য JSX বসাতে পারেন। tabs={[…]} নামের একটি বড় prop-এ এই স্বাধীনতা পাওয়া কঠিন। আর role, aria-selected লেখা হয়েছে একবারই, ভেতরে।

পুরোনো কোডে দেখবেন Children.map আর cloneElement দিয়ে সন্তানদের গোপনে prop জোড়া হচ্ছে। React-এর docs Children API-কে অপ্রচলিত ও ভঙ্গুর বলে, কারণ সন্তানকে আরেকটি component-এ মুড়ে দিলেই সেটি ভেঙে যায়। Docs বিকল্প হিসেবে একাধিক component প্রকাশ করা, object-এর array prop আর render prop-এর কথা বলে। Context-ভিত্তিক compound component এই ভঙ্গুরতা এড়ায়।

৮ · “বাজারঘর”-এর Button আর ProductCard — সবার জন্য নকশা

এবার সব জোড়া দেওয়া যাক। পুরোনো ধরনের একটি “বোতাম” দিয়ে শুরু করি, যা দেখতে ঠিক আছে, কিন্তু অনেকের কাজে লাগে না। পরীক্ষার জন্য linter Oxlint চালানো হলো। পর্ব ০৫-এ দেখা এই টুলের accessibility নিয়মগুলো (jsx-a11y) চালু করা হয়েছে।

export function AddButton({ onAdd }: { onAdd: () => void }) {
  return (
    <div>
      <img src="/dal.webp" />
      <div className="btn" onClick={onAdd}>কার্টে যোগ করুন</div>
    </div>
  );
}

সত্যিকারের run · Oxlint 1.90.0 · লম্বা বার্তা ছোট করা হয়েছে

$ npx oxlint --jsx-a11y-plugin -D jsx-a11y/alt-text \
    -D jsx-a11y/click-events-have-key-events \
    -D jsx-a11y/no-static-element-interactions src/ClickDiv.tsx
src/ClickDiv.tsx:4:7: error jsx-a11y(alt-text): Missing `alt` attribute.
src/ClickDiv.tsx:5:7: error jsx-a11y(click-events-have-key-events): Enforce a clickable non-interactive element has at least one keyboard event listener.
src/ClickDiv.tsx:5:8: error jsx-a11y(no-static-element-interactions): Static HTML elements with event handlers require a role.

মাউসে এটি ঠিকই চলে। কিন্তু div Tab চাপলে focus (keyboard এখন যে element-এ আছে) পায় না, Enter-এ সাড়া দেয় না, আর screen reader (পর্দার লেখা পড়ে শোনানোর সফটওয়্যার) একে বোতাম বলে ঘোষণা করে না। alt না থাকলে দৃষ্টিপ্রতিবন্ধী ব্যবহারকারী জানতেই পারেন না ছবিটি কিসের। সমাধান role যোগ করা নয়, আসল <button> ব্যবহার করা। তত্ত্বের জন্য HTML ও CSS কোর্সের accessibility পর্ব দেখুন। এখানে শুধু component-এর দিকটি দেখব।

export function Button({
  variant = "primary", size = "md", loading = false,
  className = "", children, disabled, ...rest
}: ButtonProps) {
  return (
    <button
      type="button"                     // form ভুলে submit হবে না
      {...rest}                         // onClick, aria-label… সব চলবে
      className={`btn btn-${variant} btn-${size} ${className}`}
      disabled={disabled || loading}
      aria-busy={loading}
    >
      {children}
    </button>
  );
}

এই কয়েক লাইনে অনেক সিদ্ধান্ত আছে। type="button" আগে, কারণ HTML-এ form-এর ভেতরে button-এর default type হলো submit. {...rest} এর পরে, যাতে ব্যবহারকারী type="submit" দিতে পারেন। className আর disabled তার পরে, যাতে ব্যবহারকারীর দেওয়া মান মিশে যায়, মুছে না দেয়। এখানে spread নিরাপদ, কারণ type-এ শুধু button-এর props আছে। আর ProductCard:

type ProductCardProps = {
  product: Product;
  badge?: ReactNode;                    // slot: “নতুন”, “২০% ছাড়”…
  onAdd: (id: number) => void;          // callback
};

export function ProductCard({ product, badge, onAdd }: ProductCardProps) {
  const inStock = product.stock > 0;
  const action = inStock ? "কার্টে যোগ করুন" : "স্টক শেষ";
  return (
    <article className="card" aria-labelledby={`p-${product.id}`}>
      <img src={product.imageUrl} alt={product.name} width={240} height={240} />
      <h3 id={`p-${product.id}`}>{product.name}</h3>
      {badge}
      <p>৳{product.price}</p>
      <Button disabled={!inStock} aria-label={`${product.name}: ${action}`}
        onClick={() => onAdd(product.id)}>{action}</Button>
    </article>
  );
}
সিদ্ধান্তকেন
showBadge নয়, badge slotলেখা, রং, আইকন সবই ব্যবহারকারীর হাতে
onAdd শুধু id পাঠায়কার্ট কীভাবে চলে, কার্ড তা জানে না
alt={product.name}ছবিটি কিসের, তা সবাই জানতে পারেন
width, heightছবি লোডের সময় পাতা লাফায় না (CLS)
aria-label-এ পণ্যের নাম২০টি “কার্টে যোগ করুন”-এর ভিড়ে screen reader-এ কোনটি কোন পণ্যের, তা বোঝা যায়
দৃশ্যমান লেখা aria-label-এও আছেVoice control-এ দেখা লেখা বলেই বোতাম চাপা যায়

একটি reusable component-এর accessibility-র বড় অংশ তার API-র মধ্যেই। IconButton-এ aria-label ঐচ্ছিক না রেখে বাধ্যতামূলক করলে কেউ আর লেবেল ভুলতে পারবেন না, কারণ tsc থামিয়ে দেবে। Type এখানে নিয়মের পাহারাদার। মনে রাখবেন, linter সব ধরে না: রঙের contrast বা Tab-এর ক্রম আপনাকে keyboard আর screen reader দিয়ে নিজে পরীক্ষা করতে হবে।

৯ · Prop drilling — যে সমস্যা পরের মডিউল সারাবে

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

“বাজারঘর”-এর হেডারে কার্টের সংখ্যা দেখাতে গিয়ে একই ঘটনা ঘটে। সংখ্যাটি আছে সবার ওপরে, লাগবে সবার নিচে। মাঝের component-গুলো এটি ব্যবহার করে না, তবু নিজের props-এ নিতে হয়। একেই বলে prop drilling.

একটি মান, তিনটি হাত

cartCount passes through components that never use it

  1. AppcartCountমালিক
  2. LayoutcartCountশুধু বহন
  3. HeadercartCountশুধু বহন
  4. CartBadgecartCountআসল ব্যবহারকারী
ছবি ৯.২ — ৪টি component, ৩টি ধাপ, ২ জন বাহক।

হিসাবটি সরল: dটি component-এর শিকলে prop পার হয় d − 1টি ধাপ, আর বাহক থাকে d − 2 জন। এখানে d = ৪, তাই ৩ ধাপ আর ২ জন বাহক। কিন্তু দাম সংখ্যায় নয়, বদলের সময়ে। Prop-এর নাম বা type বদলালে প্রতিটি ধাপ ছুঁতে হয়। আর Layout-এর type-এ এমন একটি জিনিস ঢুকে গেল, যার সঙ্গে তার কোনো সম্পর্ক নেই। এতে coupling বাড়ে।

দুই-তিন ধাপের drilling কোনো অপরাধ নয়। এটি স্পষ্ট, আর সহজে খুঁজে পাওয়া যায়। অনেক সময় সমাধান এই পর্বের ভেতরেই আছে: composition.

// আগে: Layout আর Header দুজনেই cartCount বহন করে
<Layout cartCount={count} />

// পরে: CartBadge ওপরেই তৈরি, children/slot হয়ে নামে
<Layout header={<Header cart={<CartBadge count={count} />} />}>
  <ProductListPage />
</Layout>

এখন Layout আর Header সংখ্যাটির কথা জানেই না। তারা শুধু নিজের ফাঁকা জায়গায় যা পায় তা বসায়। React-এর docs-ও context ধরার আগে এই পথ চেষ্টা করতে বলে। তবু ভাষা, লগইন করা ব্যবহারকারী বা theme-এর মতো মান যখন সত্যিই বহু দূরের বহু component-এ লাগে, তখন পর্ব ১৭ · Context আর useReducer এবং পর্ব ১৮ · Zustand আর Redux Toolkit কাজে আসে। আর state ঠিক কোথায় রাখবেন, তার সিদ্ধান্ত আছে পর্ব ১৬-এ।

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

  • Props আর state-এর পার্থক্য? Props বাইরে থেকে আসে, component পড়ে। State component নিজে রাখে আর বদলের অনুরোধ করে।
  • সন্তান বাবাকে খবর দেয় কীভাবে? বাবা একটি function prop পাঠায়, সন্তান সেটি ডাকে।
  • Props না বদলালেও সন্তান re-render হয়? হ্যাঁ, বাবা render হলে ডিফল্টভাবে সন্তানও হয়, memo না থাকলে।
  • Prop drilling কী, সমাধান কী? মাঝের component দিয়ে অব্যবহৃত prop বহন। সমাধান composition, context বা store.
  • Composition inheritance-এর চেয়ে কেন পছন্দের? React-এ JSX আর props দিয়েই বিশেষায়ন করা যায়, শ্রেণিবিন্যাসের শক্ত বাঁধন লাগে না।

১০ · AI-এর যুগে — component নকশা যাচাই

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

AI-এর সাধারণ ভুলকীভাবে ধরবেন
Function component-এ defaultPropsReact 19-এ এটি function component থেকে সরানো হয়েছে; default parameter লিখুন
নতুন কোডে propTypesReact 19 এগুলো নীরবে উপেক্ষা করে; TypeScript ব্যবহার করুন
পুরোনো forwardRef মোড়কReact 19-এ ref সাধারণ prop; নিজের version-এর docs মিলিয়ে নিন
props: any আর সবখানে {...props}any খুঁজুন, spread-এর উৎস দেখুন
<div onClick>, alt ছাড়া ছবিOxlint-এর jsx-a11y চালান, Tab চেপে পরীক্ষা করুন
একটি বোতামের জন্য নতুন UI librarypackage.json-এর diff দেখুন; প্রকৃত দরকার জিজ্ঞেস করুন
অস্তিত্বহীন prop বা option বানানোtsc চালান, library-র docs পড়ুন
Secret বা কেনা দাম props-এ ক্লায়েন্টে পাঠানোHTML-এর source দেখুন; server থেকে যা client-এ যায় তা গোপন নয় (পর্ব ৫২)

AI-কে শুধু “একটি কার্ড বানাও” না বলে চুক্তি আগে দিন, যাতে নকশার সিদ্ধান্তগুলো আপনার হাতে থাকে।

আমি React 19 আর TypeScript strict mode ব্যবহার করি। একটি ProductCard component লিখে দাও। শর্ত: ১. Props: product (id, name, price, stock, imageUrl), badge (ঐচ্ছিক ReactNode), onAdd(id: number). ২. কোনো showX ধরনের boolean flag দেবে না। any আর defaultProps ব্যবহার করবে না। ৩. আসল button আর alt ব্যবহার করবে। keyboard আর screen reader-এ কীভাবে কাজ করবে তা বলবে। ৪. নতুন কোনো package যোগ করবে না। ৫. কোডের পরে বলো: কোন React version ধরে লিখেছ, কোন prop-টি নিয়ে তুমি নিশ্চিত নও, আর আমি কোন তিনটি পরীক্ষা নিজে করব।

AI-এর লেখা component হাতে পেলে

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

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

প্র ০১Props read-only রাখার নিয়ম এত কড়া কেন? সন্তান সরাসরি বদলালে তো কোড ছোট হতো।

React যখন ২০১৩ সালে উন্মুক্ত হয়, তখন জনপ্রিয় AngularJS-এর একটি বড় আকর্ষণ ছিল two-way binding: model বদলালে view বদলায়, view বদলালে model. ছোট form-এ এটি জাদুর মতো লাগত। কিন্তু বড় পাতায় একটি মান কে বদলেছে, তা খুঁজে পাওয়া কঠিন হতো, কারণ পরিবর্তন যেকোনো দিক থেকে আসতে পারত।

React উল্টো পথ নিল: one-way data flow. একটি মান বদলাতে পারে শুধু তার মালিক। তাই “পর্দায় ভুল দাম কেন?” প্রশ্নের উত্তর খুঁজতে ট্রি ধরে ওপরে উঠলেই হয়। মালিক একজন, তাই খোঁজার পথও একটি। কোড কয়েক লাইন বড় হয়, বিনিময়ে ডেটার পথ অনুমানযোগ্য হয়।

দ্বিতীয় কারণ আরও গভীর। React ধরে নেয় component একটি pure function: একই ইনপুটে একই ফল, আর render-এর সময় বাইরের কিছু বদলায় না। আধুনিক React একটি render শুরু করে থামাতে পারে, ফেলে দিয়ে আবার শুরু করতে পারে। Development-এর Strict Mode ইচ্ছা করে render দুইবার চালায়, যাতে অপবিত্র কোড ধরা পড়ে। ওপরের Price component দুইবার চললে ১৪০ থেকে ২৮০, তারপর ৫৬০ হতো যদি বদলানো যেত। অর্থাৎ props বদলানো মানে render-এর সংখ্যার ওপর ফল নির্ভর করা, যা React-এর নকশার সঙ্গে সরাসরি সংঘাত।

Vue-ও একই সিদ্ধান্তে এসেছে: সেখানে সন্তান prop বদলালে সতর্কবার্তা আসে, আর v-model আসলে prop আর event-এর সংক্ষিপ্ত রূপ। Form-এ দুই দিকের সুবিধা তাই একমুখী প্রবাহের ওপরেই বসানো। পর্ব ১২-এ controlled input-এ React-এও একই জোড়া দেখবেন: value নিচে, onChange ওপরে।

বাস্তব কাজে এর একটি সরল পরীক্ষা আছে। পর্দায় একটি ভুল মান দেখলে React DevTools খুলে component-এর props দেখুন, তারপর এক ধাপ ওপরে উঠুন। যেখানে মানটি প্রথম ভুল হলো, ভুলটি সেখানেই।

মূল উপলব্ধি: সীমাবদ্ধতাটি ঝামেলা নয়, এটিই debug করার পথ। প্রতিটি মানের একজন মালিক থাকলে বড় অ্যাপেও কারণ খুঁজে পাওয়া যায়।

প্র ০২React class-ভিত্তিক inheritance ছেড়ে composition কেন বেছে নিল? SaleCard extends Card লিখলে সমস্যা কোথায়?

“Class inheritance-এর চেয়ে object composition পছন্দ করুন”, এই পরামর্শ React-এর অনেক আগের। ১৯৯৪ সালে “Design Patterns” বইয়ে (যার চার লেখককে Gang of Four বলা হয়) এটি লেখা হয়েছিল। কারণ, উত্তরাধিকার হলো সবচেয়ে শক্ত coupling: সন্তান class বাবার ভেতরের খুঁটিনাটির ওপর নির্ভর করে। বাবার একটি method বদলালে দূরের সন্তান ভাঙতে পারে। একে fragile base class সমস্যা বলে।

UI-তে এটি আরও স্পষ্ট। ধরুন SaleCard extends Card, আর CompactCard extends Card. এবার ছাড়ের ছোট কার্ড লাগবে। সে কার কাছ থেকে উত্তরাধিকার নেবে? JavaScript-এ একটি class-এর বাবা একজনই। একটি শ্রেণিবিন্যাস একটি অক্ষেই ভাগ করতে পারে, অথচ UI-র বৈচিত্র্য বহু অক্ষে চলে: আকার, রং, অবস্থা, পাতা। Composition-এ এগুলো স্বাধীনভাবে জোড়া যায়: <Card size="sm"><SaleBadge />…</Card>.

React-এর পুরোনো docs-এ সরাসরি লেখা ছিল, Facebook-এ হাজার হাজার component-এ React ব্যবহার করে তাঁরা component inheritance hierarchy-র কোনো উপযুক্ত ক্ষেত্র পাননি। এই বক্তব্যটি তাঁদের অভিজ্ঞতা, সব জগতের জন্য গাণিতিক প্রমাণ নয়। তবে এর পেছনের যুক্তিটি শক্ত: props আর children এত নমনীয় যে “বিশেষ ধরনের কার্ড” বানাতে উত্তরাধিকার লাগে না। SaleCard হতে পারে একটি ছোট component, যে Card-কে নির্দিষ্ট props দিয়ে ডাকে।

Composition-এরও সীমা আছে। অতিরিক্ত টুকরো করলে একটি বোতাম খুঁজতে দশটি ফাইল ঘুরতে হয়। পর্ব ০৪-এ দেখা god component-এর উল্টো রোগ হলো অকারণ abstraction. তাই তিনবার একই জিনিস দেখার পরে ভাগ করা অনেকের কাজের নিয়ম।

“বাজারঘর”-এ বিষয়টি এভাবে দেখুন: নতুন একটি “ঈদের অফার” কার্ড লাগলে কোনো পুরোনো class ছুঁতে হয় না। একটি ছোট component লিখে পুরোনো টুকরোগুলো জুড়লেই হয়, তাই পুরোনো পাতা ভাঙার ভয়ও কম থাকে।

মূল উপলব্ধি: বৈচিত্র্য যখন বহু অক্ষে, তখন শ্রেণিবিন্যাস ভাঙে আর জোড়া টিকে থাকে। প্রশ্নটি “এটি কি ওটির একটি ধরন?” নয়, “এটি কি ওটিকে ব্যবহার করে?”

প্র ০৩Mixin, HOC, render prop, Hook — একই সমস্যার চারটি সমাধান কেন এলো?

সমস্যা একটিই: একাধিক component-এর মধ্যে UI নয়, আচরণ ভাগ করা। ধরুন ১০টি component-এ মাউসের অবস্থান বা লগইন অবস্থা লাগে। React-এর ইতিহাস এই প্রশ্নের উত্তরের ইতিহাস।

  • Mixin (শুরুর যুগ): createClass-এ অন্য object-এর method মিশিয়ে দেওয়া যেত। ২০১৬ সালের জুলাইয়ে Dan Abramov React-এর ব্লগে “Mixins Considered Harmful” লেখেন। তিনটি সমস্যা: লুকানো নির্ভরতা, নামের সংঘর্ষ, আর সময়ের সঙ্গে জটিলতার বরফগোলা।
  • Higher-Order Component (HOC): একটি function component নিয়ে আরেকটি component ফেরত দেয়। সেই লেখাতেই HOC বিকল্প হিসেবে এসেছিল। কিন্তু কয়েক স্তর জড়ালে devtools-এ wrapper-এর পাহাড় হয়, আর কোন prop কে দিল তা অস্পষ্ট হয়।
  • Render prop (২০১৭-র দিকে জনপ্রিয়): একটি function prop-এর মাধ্যমে ডেটা স্পষ্টভাবে দেওয়া হয়, তাই নামের গোলমাল নেই। কিন্তু একাধিক জড়ালে JSX ডানদিকে সরে যায়।
  • Hook (React 16.8, ২০১৯): useSomething() সরাসরি মান ফেরত দেয়। Wrapper নেই, JSX-এর গভীরতা নেই। আচরণ ভাগের প্রধান পথ এখন এটিই।

তাহলে পুরোনোগুলো কি মৃত? না। Hook আচরণ ভাগ করে, কিন্তু “এই জায়গায় কী আঁকব” প্রশ্নের উত্তর দেয় না। সেজন্য renderItem এখনো টিকে আছে। পুরোনো কোডবেসে HOC-ও পাবেন, আর কিছু library-র API এখনো সেই ধরনের। AI অনেক সময় পুরোনো টিউটোরিয়াল থেকে HOC লিখে দেয়, যেখানে একটি hook-ই যথেষ্ট ছিল।

লক্ষ করুন, প্রতিটি নতুন সমাধান আগেরটির একটি নির্দিষ্ট ব্যথা সারিয়েছে: mixin-এর লুকানো ভাব, HOC-এর wrapper, render prop-এর গভীরতা। Custom hook পুরো শিখবেন পর্ব ১৪-এ।

তাই কোনো পুরোনো কোডবেসে HOC দেখলে সঙ্গে সঙ্গে সব বদলে ফেলার দরকার নেই। যে কোড ভালো চলছে আর test আছে, তাকে নতুন ফ্যাশনের জন্য ঝুঁকিতে ফেলা বুদ্ধিমানের কাজ নয়। নতুন কোডে hook দিয়ে শুরু করাই সাধারণ পথ।

মূল উপলব্ধি: কোনো pattern চিরন্তন সত্য নয়, প্রতিটি একটি সময়ের ব্যথার ওষুধ। কোন ব্যথা সারাচ্ছে তা জানলে বুঝবেন কখন সেটি আর দরকার নেই।

প্র ০৪React propTypes, defaultProps আর forwardRef-এর মতো পরিচিত API সরাল বা পাল্টাল কেন? TypeScript কি runtime যাচাইয়ের জায়গা নিল?

একসময় props যাচাইয়ের প্রধান পথ ছিল propTypes: চলার সময় development-এ console-এ সতর্কবার্তা। React 15.5 (২০১৭)-এ এটি মূল package থেকে আলাদা করার পথে যায়। আর React 19-এর upgrade guide বলে, propTypes-এর যাচাই React package থেকে সরানো হয়েছে, ব্যবহার করলে নীরবে উপেক্ষিত হবে। পরামর্শ TypeScript বা অন্য type-checking.

কারণটি ব্যবহারিক। propTypes ভুল ধরত শুধু সেই component চলার সময়, আর শুধু development-এ। TypeScript সব ব্যবহার একসাথে দেখে, চালানোর আগেই। ওপরের run-এ ৫টি ভুল এক তালিকায় এসেছিল, কোনো পাতা না খুলেই।

defaultProps-এর ক্ষেত্রেও একই যুক্তি। JavaScript-এ যখন size = "md" default parameter আছে, তখন আলাদা ব্যবস্থার দরকার কী? React 19-এ function component থেকে এটি সরানো হয়েছে; class component-এ এটি রয়ে গেছে, কারণ সেখানে ভাষায় বিকল্প নেই। ref এখন সাধারণ prop হিসেবে পাওয়া যায়, তাই নতুন কোডে forwardRef-এর মোড়ক সাধারণত লাগে না। TypeScript-এর দিকেও বদল এসেছে: React 18-এর type থেকে React.FC-এর গোপন children সরানো হয়, কারণ যে component children ব্যবহার করে না, তার ক্ষেত্রেও ভুলটি ধরা পড়ত না।

তবে TypeScript runtime যাচাইয়ের পুরো জায়গা নেয়নি। পর্ব ০৬-এ দেখেছেন, type চলার সময় থাকে না। API-র উত্তর, URL-এর query, localStorage, এগুলোর জন্য সীমানায় Zod-এর মতো যাচাই লাগবেই। ভেতরের কোডে compiler, বাইরের ডেটায় runtime check. দুটি আলাদা কাজ।

“বাজারঘর”-এ এর চেহারা সহজ। ProductCard-এর props-এ শুধু TypeScript. কিন্তু যেখানে API-র JSON প্রথম কোডে ঢোকে, সেখানে একটি schema দিয়ে যাচাই। যাচাই পার হলে তবেই ডেটা Product হিসেবে ট্রি বেয়ে নামে, আর নিচের কোনো component-কে আর সন্দেহ করতে হয় না।

মূল উপলব্ধি: ভাষা বা টুলচেইন যখন একটি কাজ ভালোভাবে করতে শেখে, framework তখন নিজের সমান্তরাল ব্যবস্থা সরিয়ে নেয়। পুরোনো কোড আর AI-এর উত্তরে তাই অচল API পাওয়া স্বাভাবিক। Version মিলিয়ে নেওয়াটাই আসল অভ্যাস।

প্র ০৫“No ARIA is better than Bad ARIA” — reusable component-এর accessibility-তে এই কথার অর্থ কী? সব কি নিজে লেখা উচিত?

W3C-এর ARIA Authoring Practices Guide-এর “Read Me First” অংশ শুরুই হয় এই কথা দিয়ে। ARIA হলো কিছু attribute, যা সহায়ক প্রযুক্তিকে element-এর ভূমিকা জানায়। কিন্তু ARIA শুধু প্রতিশ্রুতি দেয়, আচরণ দেয় না। <div role="button"> লিখলে screen reader “বোতাম” বলবে, অথচ Tab-এ focus আসবে না, Enter-এ কিছু হবে না। ব্যবহারকারীকে মিথ্যা বলা হলো। এর চেয়ে আসল <button> অনেক ভালো, কারণ focus, Enter, Space আর ভূমিকা বিনা খরচে আসে।

Reusable component-এ এই সিদ্ধান্তের ওজন বহুগুণ। একটি পাতার ভুল একটি পাতায় থাকে। Button-এর ভুল থাকে সব পাতায়। উল্টো দিকে, Button একবার ঠিক করলে সব পাতা ঠিক হয়ে যায়। Component-ভিত্তিক UI-এর সবচেয়ে বড় উপহার সম্ভবত এটিই।

কিন্তু কিছু widget সত্যিই কঠিন। APG-এর Tabs pattern অনুযায়ী Tab চাপলে focus সক্রিয় tab-এ আসে, Left আর Right Arrow দিয়ে tab বদলায়, আর শেষ থেকে আবার শুরুতে ঘোরে। Combobox, modal dialog, date picker-এ দায়িত্ব আরও বেশি: focus আটকে রাখা, ফিরিয়ে দেওয়া, Escape সামলানো। ওপরের Tabs-এ arrow key এখনো নেই। এজন্য একটি ধারা গড়ে উঠেছে: headless component. এগুলো আচরণ আর accessibility দেয়, কিন্তু কোনো চেহারা দেয় না। Radix UI, React Aria, Headless UI এই ধারার উদাহরণ। এটি composition-এর চরম রূপ: আচরণ এক জায়গায়, রং আরেক জায়গায়। কোনোটি একমাত্র সঠিক পছন্দ নয়, আর পর্ব ২২-এ styling-এর সঙ্গে তুলনা দেখবেন।

তাহলে নিয়মটি কী? সাধারণ button, link, input-এ native HTML. জটিল widget-এ আগে যাচাই করুন, ভালোভাবে পরীক্ষিত library আছে কি না। নিজে লিখলে APG পড়ে keyboard আর screen reader দিয়ে পরীক্ষা করুন। আর স্বয়ংক্রিয় linter পাহারা দিলেও সব সমস্যা ধরে না।

নিজে পরীক্ষা করা কঠিন নয়। মাউস সরিয়ে রেখে শুধু Tab, Enter আর Space দিয়ে একটি পণ্য কার্টে যোগ করে দেখুন।

মূল উপলব্ধি: Accessibility পরে লাগানোর রং নয়, component-এর API-র অংশ। একবার ঠিক করে পুনর্ব্যবহার করাই সবচেয়ে সস্তা পথ।

অনুশীলন

হিসাব, নকশা আর পূর্বাভাস। আগে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হিসাব: একটি Button-এ ৭টি boolean flag আছে: primary, secondary, danger, ghost, small, large, loading. (ক) মোট কয়টি অবস্থা লেখা যায়? (খ) রং ঠিক একটি, আকার small, মাঝারি বা large, আর loading হ্যাঁ বা না হলে অর্থপূর্ণ অবস্থা কয়টি? (গ) অর্থহীন কয়টি? (ঘ) ৩০টি কার্ডের grid-এ প্রতিটি কার্ডে ২টি সন্তান থাকলে একবার re-render-এ কয়টি function ডাক হবে?

    (ক) 27 = ১২৮. (খ) রং ৪ রকম, আকার ৩ রকম, loading ২ রকম: ৪ × ৩ × ২ = ২৪. (গ) ১২৮ − ২৪ = ১০৪টি অর্থহীন। variant, size আর একটি loading ব্যবহার করলে লেখা যায় ঠিক ২৪টিই। (ঘ) ৩n + ১ = ৩ × ৩০ + ১ = ৯১টি।

  2. আউটপুট অনুমান করুন: function P({ size = 100 }) { console.log(size); return null; } আর function Q(props) { console.log(props.a); return null; } লেখা আছে। নিচের প্রতিটি render করলে ক্রমে কী ছাপা হবে?
    <P />
    <P size={undefined} />
    <P size={null} />
    <P size={0} />
    <Q a={2} {...{ a: 1 }} />
    <Q {...{ a: 1 }} a={2} />
    100
    100
    null
    0
    1
    2

    Default বসে শুধু অনুপস্থিত বা undefined মানে। null আর 0 সত্যিকারের মান। শেষ দুই লাইনে পরে লেখা জিনিসটি জেতে: পঞ্চমটিতে spread পরে, তাই 1; ষষ্ঠটিতে a={2} পরে, তাই 2. (এই ফল React 19.3.1-এ চালিয়ে মিলিয়ে নেওয়া।)

  3. নকশা ও সমালোচনা: একজন সহকর্মী লিখেছেন <Modal isOpen isConfirm isDanger showClose showFooter hideTitle confirmText="…" onConfirm={…} />. এর তিনটি সমস্যা বলুন, আর composition দিয়ে নতুন নকশা প্রস্তাব করুন।

    সমস্যা: (১) ৬টি boolean মানে ৬৪ অবস্থা, অনেকগুলো অর্থহীন, যেমন showFooter ছাড়া isConfirm. (২) onConfirm কি isConfirm ছাড়াও লাগবে? চুক্তি অস্পষ্ট। (৩) ফুটারে তৃতীয় বোতাম লাগলে আবার নতুন prop লাগবে।

    <Modal open={open} onClose={close} labelledBy="del-title">
      <Modal.Title id="del-title">পণ্যটি মুছবেন?</Modal.Title>
      <p>এই কাজ ফেরানো যাবে না।</p>
      <Modal.Footer>
        <Button variant="secondary" onClick={close}>বাতিল</Button>
        <Button variant="danger" onClick={remove}>মুছুন</Button>
      </Modal.Footer>
    </Modal>

    open আর onClose prop হিসেবেই ঠিক আছে, কারণ এগুলো আচরণ। ভেতরের কাঠামো composition. এভাবে Focus আটকে রাখা আর Escape সামলানো Modal-এর ভেতরেই একবার লেখা যায়।

  4. Keyboard অডিট: ওপরের tabs.html block-এ Tab key চেপে দেখুন কী হয়। APG অনুযায়ী Left আর Right Arrow কাজ করা উচিত। ৩টি tab থাকলে Right Arrow-এ পরের index-এর সূত্র কী, Left Arrow-এর সূত্র কী? শেষ tab (i = 2)-এ Right আর প্রথম tab (i = 0)-এ Left চাপলে কোথায় যাবে? আর এই logic কোথায় লিখবেন?

    Right: (i + 1) % n. Left: (i - 1 + n) % n. n = ৩ ধরে i = ২-তে Right দিলে (২ + ১) % ৩ = ০, অর্থাৎ প্রথম tab. i = ০-তে Left দিলে (০ − ১ + ৩) % ৩ = ২, অর্থাৎ শেষ tab. Left-এ + n না দিলে JavaScript-এ -1 % 3 হয় -1, যা ভুল index.

    এই logic TabList-এর একটি onKeyDown-এ লিখতে হবে, কারণ সে-ই সব tab ধরে রাখে। ভেতরে ঘটনাটি ধরে পরের tab-এ focus দিলেই হবে। লক্ষ করুন, এই বদলে ব্যবহারকারীর JSX-এ একটি অক্ষরও বদলাতে হবে না। Compound component-এর লাভ এটিই: সব পাতার Tabs একসাথে ঠিক হয়ে যায়।

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

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

ক্রম“কার্টে যোগ করুন” ক্লিকের পরের ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ব্যবহারকারী সন্তান component-এর বোতাম চাপলেন
  2. বোতামের onClick চলল
  3. সন্তান props থেকে পাওয়া onAdd(id) ডাকল
  4. বাবার function state বদলের অনুরোধ জানাল
  5. বাবা আবার render হলো
  6. সন্তান নতুন props পেয়ে আবার render হলো

function Tag({ size = 100 }) component-কে <Tag size={null} /> দিয়ে ডাকলে ভেতরে size-এর মান কত?

  • 100
  • null
  • undefined
  • 0

একটি Parent-এর state বদলাল। তার Child কোনো prop পায় না, আর memo ব্যবহার করা হয়নি। কী ঘটবে?

  • Child চলবেই না, কারণ prop বদলায়নি
  • Child-এর DOM মুছে নতুন করে তৈরি হবে
  • Child function আবার চলবে, কিন্তু পার্থক্য না থাকলে DOM বদলাবে না
  • React একটি error দেবে

React-এ <Card><p>দাম</p></Card> লিখলে Card ভেতরের p কীভাবে পায়?

  • children নামের prop হিসেবে
  • DOM থেকে querySelector দিয়ে খুঁজে
  • Context থেকে স্বয়ংক্রিয়ভাবে
  • Card-এর state হিসেবে

একটি component-এ ৫টি স্বাধীন boolean prop থাকলে মোট কয় রকম অবস্থা লেখা যায়?

  • ৫
  • ১০
  • ১৬
  • ৩২

পুনর্ব্যবহারযোগ্য বোতাম বানাতে <div onClick={f}>-এর বদলে <button type="button" onClick={f}> কেন ভালো?

  • Button অনেক দ্রুত render হয়
  • React div-এ onClick চালায় না
  • Keyboard focus, Enter ও Space আর screen reader-এর ভূমিকা নিজে থেকেই পাওয়া যায়
  • TypeScript div-এ onClick লিখতে দেয় না

API থেকে আসা অজানা JSON object data-কে <div {...data} /> লেখার সবচেয়ে বড় ঝুঁকি কী?

  • পাতা সামান্য ভারী হবে
  • বাইরের ডেটা dangerouslySetInnerHTML-এর মতো prop বসিয়ে অচেনা HTML ঢোকাতে বা গোপন ঘর ফাঁস করতে পারে
  • TypeScript সবসময় compile থামিয়ে দেবে, তাই build হবে না
  • React spread ব্যবহার করলে সব সময় error দেয়

শব্দকোষ

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

মনে রাখুন

  1. Props হলো component-এর read-only ইনপুট; development-এ React পাহারা দেয়, production-এ দেয় না।
  2. ডেটা props হয়ে নামে, খবর callback হয়ে ওঠে। বাবা render হলে সন্তানও ডিফল্টভাবে render হয়।
  3. children আর slot prop কাঠামোকে ভেতরের জিনিস থেকে আলাদা করে। JSX নিজেই একটি মান।
  4. n boolean মানে 2n অবস্থা; পছন্দে variant, কাঠামোয় composition; অজানা object-এ spread নয়।
  5. Reusable component-এ আসল HTML element, alt আর keyboard আগে; অতিরিক্ত prop drilling-এর উত্তর composition, তারপর context.

পরের পর্বে: Props বাইরে থেকে আসে। কিন্তু count-এর মতো মানগুলো থাকে কোথায়, বদলায় কীভাবে? পরের পর্বে useState, event আর snapshot, আর দেখবেন ক্লিকের পরেই মান কেন বদলায় না।

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

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