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

Server Component আর Client Component — কোনটি কোথায় চলে

Server and Client Components — Where Each Runs, the "use client" Boundary, Serializable Props and the RSC Payload

Taught in Bangla, this lesson explains React Server Components and Client Components, where each runs and what each can do, the "use client" directive as a module-graph boundary rather than a label, which props can cross it, passing server content through children, the RSC payload in the network tab, a measured bundle difference for a markdown library on server versus client, guarding secrets with server-only and env rules, and third-party components and context.

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

  • React Server Components কী, আর Next.js তাকে কীভাবে ব্যবহার করে
  • কোন component কোথায় চলে, কী পারে আর কী পারে না, আর সেটা নিজে যাচাই করার উপায়
  • "use client" সীমানা, serializable props আর children দিয়ে server অংশ client-এর ভেতরে বসানো
  • নেটওয়ার্ক ট্যাবে আসল RSC payload পড়া, আর একটি build-এ bundle-এর পার্থক্য মাপা
  • server-only, context provider, তৃতীয় পক্ষের component আর “বাজারঘর”-এর পণ্য পাতার জন্য সিদ্ধান্তের ছক

দামের একক নিয়ে: এই পর্বের ছোট ল্যাবে উদাহরণ সহজ রাখতে দাম পূর্ণ টাকায় রাখা হয়েছে, আর আউটপুটগুলোও সেই run-এর। আসল দোকানে টাকা রাখুন পয়সায়, পূর্ণসংখ্যা হিসেবে, যেমনটি পর্ব ৩২-এ দেখেছেন।

১ · দুই রান্নাঘরের রেস্তোরাঁ: React Server Components কী

পর্ব ৫১-এ (Next.js App Router) আপনি “বাজারঘর”-এর ফোল্ডার, layout আর route বানিয়েছেন। সেখানে একটি কথা চুপচাপ সত্যি ছিল: app/-এর প্রতিটি page আর layout ডিফল্টভাবে Server Component. এই পর্বে সেই কথাটি খুলে দেখব। কোন কোড সার্ভারে থাকে, কোন কোড গ্রাহকের ফোনে যায়, আর ভুল হলে কী ভাঙে।

পুরান ঢাকার একটি রেস্তোরাঁ ভাবুন। ভেতরের রান্নাঘরে চুলা, মসলার গুদাম আর হিসাবের খাতা আছে। গ্রাহক সেখানে ঢোকেন না; তিনি পান রান্না করা খাবার। টেবিলে অবশ্য একজন ওয়েটার থাকেন, যিনি গ্রাহকের কথা শোনেন, পানি দেন, বিল আনেন। Server Component হলো ভেতরের রান্নাঘর: database, secret আর ভারী library সেখানে থাকে, আর বাইরে যায় শুধু তৈরি ফল। Client Component হলো টেবিলের ওয়েটার: তিনি গ্রাহকের পাশে থাকেন, ক্লিক আর টাইপ সামলান। ওয়েটারকে টেবিলে পাঠাতে খরচ আছে, তাই যত কম লাগে তত ভালো।

React Server Components (সংক্ষেপে RSC) Next.js-এর নিজের আবিষ্কার নয়; এটি React-এর একটি বৈশিষ্ট্য। react.dev-এর reference অনুযায়ী Server Component হলো এমন component, যা bundle করার আগেই, client app বা SSR server থেকে আলাদা একটি পরিবেশে render হয়। আর "use client" হলো React-এর একটি directive, যা module graph-এ server আর client-এর মাঝে সীমানা টানে। এই বৈশিষ্ট্য চালাতে একটি framework বা bundler লাগে, কারণ কোন ফাইল কোন দিকে যাবে তা build-এর সময় ঠিক করতে হয়। Next.js-এর App Router সেই কাজটি করে; React Router, Waku বা Parcel-এর মতো আরও কিছু টুলও RSC সমর্থন করে।

২০২০ সালের ডিসেম্বরে React দল (Dan Abramov ও Lauren Tan) “Zero-Bundle-Size React Server Components” নামে একটি পরীক্ষামূলক ধারণা দেখায়। ২০২২ সালের অক্টোবরে Next.js 13 app/ ফোল্ডার beta হিসেবে আনে, আর ২০২৩ সালের মে মাসে Next.js 13.4-এ App Router স্থিতিশীল ঘোষিত হয়। ২০২৪ সালের ডিসেম্বরে React 19 স্থিতিশীল হলে Server Components React-এর অফিসিয়াল অংশ হয়। লেখার সময়ে আমরা Next.js 16.4.4 আর React 19.3.1 ব্যবহার করেছি।

আগের মডিউলগুলোর ভাষায় বললে, পর্ব ০৩-এ CSR, SSR আর SSG-এর মানচিত্র দেখেছেন। সেগুলো প্রশ্ন করে “HTML কখন আর কোথায় তৈরি হয়”। RSC একটি আলাদা প্রশ্ন করে: কোন component-এর কোড ব্রাউজারে যাবে? একটি SSR পাতাতেও আগে প্রতিটি component-এর কোড ব্রাউজারে যেত, কারণ hydration-এর সময় সব আবার চালাতে হতো। RSC বলে, যে অংশ ক্লিক বা state চায় না, তার কোড ব্রাউজারে পাঠানোর দরকারই নেই।

“বাজারঘর”-এর পণ্য পাতা: কোন অংশ কোথায়

Server Components (blue) and Client Components (green) in one tree

পণ্য পাতার component tree: server আর client অংশ RootLayout server · html, body CartProvider client · context, children পায় ProductPage server · async, await getProduct() Gallery client · ছবি বদল দাম ৳৪,৫০০ server-এ তৈরি AddToCart client · onClick Reviews server · লেখা marked + sanitize-html Server Component: কোড ব্রাউজারে যায় না Client Component: কোড bundle-এ যায়
ছবি ৫২.১ — একই tree-তে দুই জাতের component. সবুজ দ্বীপগুলো ছোট রাখাই মূল কৌশল; ভারী library (কমলা) নীল অংশের নিচে থাকলে ব্রাউজারে যায় না।
মূল ধারণা

“Server Component” কথাটি বলে কোড কোথায় চলে আর ব্রাউজারে যায় কি না। “Server-rendered” কথাটি বলে HTML কোথায় তৈরি হলো। দুটি আলাদা প্রশ্ন। Client Component-ও সার্ভারে HTML বানায়, কিন্তু তার কোড ব্রাউজারেও যায়।

২ · কে কোথায় চলে, কে কী পারে

Next.js-এর ডকুমেন্টেশন একটি ছোট ছক দেয়। Server Component চলে শুধু সার্ভারে। Client Component চলে সার্ভারে এবং ব্রাউজারে। “Client” নামটি ভুল বোঝায়, তাই নিজের চোখে দেখা দরকার। “বাজারঘর”-এর পাতায় আমরা দুটি console.log রাখলাম: একটি ProductPage-এ (server), একটি AddToCart-এ (client). তারপর next start চালিয়ে ব্রাউজারে সরাসরি পাতাটি খুললাম।

সত্যিকারের run: Next.js 16.4.4 (Turbopack), React 19.3.1, Node.js 24, Windows-এ। project-এর পথ ~/bazarghar হিসেবে দেখানো হয়েছে। সার্ভার শুধু 127.0.0.1-এ চালানো হয়েছিল।

~/bazarghar$ npx next start -p 3852
▲ Next.js 16.4.4
- Local:         http://127.0.0.1:3852
✓ Ready in 155ms
ProductPage render jamdani
AddToCart render jamdani

সার্ভারের terminal-এ দুটি লাইনই এসেছে। অর্থাৎ AddToCart একটি Client Component হয়েও সার্ভারে চলেছে, যাতে প্রথম HTML-এ বোতামটি থাকে। ব্রাউজারের console খুললে শুধু AddToCart render jamdani দেখা যায়, কারণ hydration-এর সময় সেটি আবার চলে। ProductPage-এর লাইন ব্রাউজারে কখনো আসে না, কারণ তার কোডই ব্রাউজারে নেই।

কাজServer ComponentClient Component
async function, ভেতরে awaitহ্যাঁনা (promise পেলে use দিয়ে পড়া যায়)
Database, file, ভেতরের service সরাসরি পড়াহ্যাঁনা, API বা Server Action লাগে
Secret ব্যবহার (DB password, API key)হ্যাঁ, ব্রাউজারে যায় নানা, যা দেবেন সবাই দেখবে
useState, useReducer, useEffectনাহ্যাঁ
onClick, onChange-এর মতো event handlerনাহ্যাঁ
window, localStorage, geolocationনাহ্যাঁ, তবে শুধু effect বা handler-এর ভেতরে
useContext দিয়ে context পড়ানাহ্যাঁ
নিজের কোড bundle-এ যায়?নাহ্যাঁ

শেষ সারির আগের একটি সূক্ষ্ম কথা: Client Component সার্ভারেও চলে, তাই তার render-এর ভেতরে সরাসরি window.innerWidth পড়লে সার্ভারে error হয়। ব্রাউজারের API শুধু useEffect বা event handler-এ রাখুন (পর্ব ১৩)। সার্ভার আর ব্রাউজারের HTML না মিললে কী হয়, সেটি পর্ব ৫৬-এর (Hydration) বিষয়।

আর Server Component নিজে async হতে পারে, এটি তার সবচেয়ে বড় সুবিধা। আমাদের পাতায় await getProduct(slug) সরাসরি render-এর ভেতরে। কোনো useEffect, কোনো loading state, কোনো আলাদা API route লাগেনি। Next.js 16-এ params একটি promise, তাই আগে await params করতে হয়; পুরোনো টিউটোরিয়ালে params.slug সরাসরি পড়া দেখলে বুঝবেন সেটি Next.js 14 বা আগের।

import { notFound } from 'next/navigation';
import { getProduct } from '@/lib/db';
import Gallery from '@/app/ui/gallery';
import AddToCart from '@/app/ui/add-to-cart';
import Reviews from '@/app/ui/reviews';

export default async function ProductPage({ params }) {
  const { slug } = await params;
  const p = await getProduct(slug);
  if (!p) notFound();
  console.log('ProductPage render', slug);
  return (
    <main>
      <h1>{p.name}</h1>
      <Gallery images={p.images}>
        <p>দাম: ৳{p.price.toLocaleString('bn-BD')}</p>
      </Gallery>
      <AddToCart product={{ slug: p.slug, name: p.name, price: p.price }} />
      <Reviews reviews={p.reviews} />
    </main>
  );
}

এই ফাইলে "use client" নেই, তাই এটি Server Component. সে তিনটি component import করেছে। তার মধ্যে Gallery আর AddToCart-এর মাথায় "use client" আছে, Reviews-এর নেই। পরের অংশে দেখব, এই একটি লাইনই কীভাবে পুরো ছবি বদলে দেয়।

৩ · "use client" একটি সীমানা, প্রতিটি component-এর লেবেল নয়

নতুনরা প্রায়ই ভাবেন, প্রতিটি interactive component-এর মাথায় "use client" লিখতে হবে, আর না লিখলে সেটি server. কথাটি অর্ধেক ঠিক। আসল নিয়ম হলো: "use client" module graph-এ একটি দরজা। যে ফাইলের মাথায় এটি থাকে, সেই ফাইল আর সে যা যা import করে, সবই client bundle-এ যায়, তাদের মাথায় directive থাকুক বা না থাকুক।

শাহজালাল বিমানবন্দরের ইমিগ্রেশন ডেস্ক ভাবুন। একবার ডেস্ক পার হলে আপনার সাথে থাকা সব ব্যাগ ওপারে চলে যায়; প্রতিটি ব্যাগের জন্য আলাদা ডেস্ক নেই। "use client" সেই ডেস্ক। যে ফাইল ডেস্ক পার হলো, তার import করা সব ফাইল তার “ব্যাগ”, সবাই ব্রাউজারে যায়।

এখান থেকে দুটি ফল আসে। প্রথমত, client subtree-এর ভেতরে আর directive লেখার দরকার নেই; প্রবেশপথে একবারই যথেষ্ট। দ্বিতীয়ত, একটি ভুল জায়গায় বসানো "use client" অনেক ভারী কোড টেনে নিতে পারে। নিচের ধাপে “বাজারঘর”-এর আসল build-এর সংখ্যা দেখুন।

page.jsserver
reviews.jsmarkdown → HTML
gallery.js"use client"
add-to-cart.js"use client"
marked + sanitize-htmlreviews.js-এর import
পাতার chunk692 B → 230,210 B
ReviewVote.jsশুধু ছোট বোতাম client
  1. শুরু: সব ঠিকঠাক। page.js আর reviews.js server-এ। শুধু gallery.js আর add-to-cart.js সবুজ। Build-এ এই পাতার নিজস্ব JS chunk মাত্র 692 byte.
  2. একজন সহকর্মী লিখলেন "use client". Review-এর পাশে একটি “সহায়ক” বোতাম দরকার ছিল, তাই তিনি reviews.js-এর মাথায় directive বসালেন। এখন reviews.js client subtree-এর প্রবেশপথ।
  3. Import-গুলোও সাথে গেল। reviews.js import করে marked আর sanitize-html. Directive তাদের ফাইলে না থাকলেও দুটিই এখন client module graph-এর অংশ।
  4. Build-এর মাপ। পাতার chunk 692 byte থেকে 230,210 byte. পুরো পাতার JS (gzip, level 9) 171.0 KB থেকে 247.8 KB, অর্থাৎ ৭৬.৮ KB বেশি। ক্রেতার ফোন এখন markdown parser নামায়, অথচ review-এর লেখা বদলায়ই না।
  5. সমাধান: সীমানা নিচে নামান। reviews.js-কে আবার server রাখুন, আর শুধু বোতামের জন্য ছোট ReviewVote.js বানিয়ে তাতে "use client" দিন। ভারী library সার্ভারেই থাকে, ব্রাউজারে যায় কয়েকশো byte.

এই হিসাব হাতে করতে একটি ছোট মডেল লিখি। নিচের কোড একটি module graph নেয়, server অংশ থেকে হেঁটে প্রথম client চিহ্নিত ফাইল খোঁজে, আর সেখান থেকে যা যা import হয় সব “পাঠানো হবে” তালিকায় তোলে। আকারগুলো আমাদের build থেকে আনুমানিক (marked-এর ESM ফাইল প্রায় 47 KB; বাকিটা sanitize-html আর তার dependency). এটি Next.js-এর আসল bundler নয়, শিক্ষামূলক মডেল।

// Module graph-এর শিক্ষামূলক মডেল। kb = এক build-এর মাপ থেকে আনুমানিক
const graph = {
  'page.js':       { kb: 1,   imports: ['db.js', 'gallery.js', 'add-to-cart.js', 'reviews.js'] },
  'db.js':         { kb: 2,   imports: [] },
  'gallery.js':    { kb: 1,   imports: [], client: true },
  'add-to-cart.js':{ kb: 1,   imports: ['cart.js'], client: true },
  'cart.js':       { kb: 1,   imports: [], client: true },
  'reviews.js':    { kb: 1,   imports: ['marked', 'sanitize-html'] },
  'marked':        { kb: 47,  imports: [] },
  'sanitize-html': { kb: 178, imports: [] },
};
function shipped(g) {
  const out = new Set();
  const visit = (m) => {                    // সীমানার ভেতরে ঢুকলে সব import client-এ
    if (out.has(m)) return;
    out.add(m);
    g[m].imports.forEach(visit);
  };
  const walk = (m, seen = new Set()) => {   // server অংশে হাঁটা, সীমানা খোঁজা
    if (seen.has(m)) return;
    seen.add(m);
    if (g[m].client) return visit(m);
    g[m].imports.forEach((x) => walk(x, seen));
  };
  walk('page.js');
  const list = [...out];
  return list.join(', ') + ' = ' + list.reduce((s, m) => s + g[m].kb, 0) + ' KB';
}
console.log('reviews server-এ :', shipped(graph));
graph['reviews.js'].client = true;          // reviews.js-এর মাথায় 'use client'
console.log('reviews client-এ :', shipped(graph));
graph['page.js'].client = true;             // পুরো পাতাই 'use client'
console.log('পুরো পাতা client :', shipped(graph));

আউটপুট:

reviews server-এ : gallery.js, add-to-cart.js, cart.js = 3 KB
reviews client-এ : gallery.js, add-to-cart.js, cart.js, reviews.js, marked, sanitize-html = 229 KB
পুরো পাতা client : page.js, db.js, gallery.js, add-to-cart.js, cart.js, reviews.js, marked, sanitize-html = 232 KB

প্রথম লাইনে শুধু তিনটি ছোট client ফাইল। দ্বিতীয় লাইনে reviews.js-এ একটি পতাকা বসাতেই marked আর sanitize-html তালিকায় ঢুকল, ৩ KB থেকে ২২৯ KB. তৃতীয় লাইন সবচেয়ে বিপজ্জনক: পুরো পাতা client হলে db.js-ও তালিকায়। সেখানে database-এর password থাকে। অংশ ৮-এ দেখবেন, আসল Next.js-এ এই ভুল কীভাবে ধরা যায়।

উল্টো দিকটিও মনে রাখুন: Client Component একটি Server Component import করলে সেটি আর Server Component থাকে না; সেটিও client graph-এ ঢুকে যায়। তখন তার ভেতরের async বা database কোড ভাঙবে। Client-এর ভেতরে server অংশ চাইলে import নয়, children বা অন্য prop দিয়ে পাঠাতে হয়। সেটি অংশ ৫-এ।

৪ · সীমানা পার হয় শুধু serializable props

Server Component সার্ভারে চলে, Client Component ব্রাউজারে। মাঝখানে নেটওয়ার্ক। তাই server থেকে client-কে যা props দেবেন, সেটি লেখায় বদলে (serialize করে) পাঠাতে হয়, আর ওপারে আবার object বানাতে হয়। JSON-এর মতো, তবে React-এর নিজের নিয়মে, যা JSON-এর চেয়ে একটু উদার।

ঢাকা থেকে রাজশাহীতে কুরিয়ারে কী পাঠানো যায়? কাপড়, বই, কাগজের নোট। কিন্তু “আমার রান্নার হাত” পাঠানো যায় না; পাঠাতে হলে রেসিপি লিখে দিতে হয়। Function হলো সেই রান্নার হাত। সার্ভারের function-এর ভেতরে সার্ভারের ভ্যারিয়েবল, connection, closure জড়িয়ে থাকে। কুরিয়ারের প্যাকেটে সেটি ঢোকে না।

react.dev-এর তালিকা অনুযায়ী যা যায় (serializable): string, number, bigint, boolean, undefined, null; Symbol.for দিয়ে নিবন্ধিত symbol; serializable জিনিসের Array, Map, Set, TypedArray আর ArrayBuffer; Date; object initializer দিয়ে বানানো সাধারণ object; Server Function ("use server" চিহ্নিত); Server বা Client Component-এর JSX element; আর Promise. যা যায় না: সাধারণ function, class, built-in ছাড়া অন্য যেকোনো class-এর object, null prototype-এর object, আর নিবন্ধনহীন symbol.

এবার আসল error দেখি। প্রতিটি পরীক্ষা next dev-এ চালানো, কারণ production-এ error বার্তার বিস্তারিত লুকানো থাকে। প্রথমে AddToCart-কে একটি সাধারণ arrow function দিলাম:

<AddToCart product={…} onAdded={() => console.log("added")} />
ProductPage render jamdani
⨯ Error: Event handlers cannot be passed to Client Component props.
  <... product={{...}} onAdded={function onAdded}>
                               ^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
 GET /products/jamdani 500 in 902ms

নাম on দিয়ে শুরু, তাই React ধরে নিল এটি event handler. নাম বদলে formatPrice={(n) => "৳" + n} দিলে বার্তাও বদলায়:

⨯ Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or maybe you meant to call this function rather than return it.
  <... product={{...}} formatPrice={function formatPrice}>

তৃতীয় পরীক্ষা: দামকে একটি class-এর object বানালাম, class Taka { constructor(v) { this.v = v; } }. বাস্তবে এটি প্রায়ই ঘটে, কারণ ORM অনেক সময় দশমিক দামকে একটি class-এর object হিসেবে দেয় (decimal type, পর্ব ৩৯)।

⨯ Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.
  {slug: "jamdani", name: ..., price: {v: 4500}}
                                      ^^^^^^^^^

আর Date? JSON-এ Date হারিয়ে string হয়ে যায়, কিন্তু React Date চেনে। listedAt: new Date('2026-11-01T04:00:00Z') দিয়ে পাতাটি ঠিকঠাক চলল (200). Payload-এ সেটি এভাবে গেল, সামনে $D চিহ্ন, যাতে ওপারে আবার Date বানানো যায়:

"product":{"slug":"jamdani","name":"জামদানি শাড়ি","price":4500,"listedAt":"$D2026-11-01T04:00:00.000Z"}

এই পাহারাদার আসলে কীভাবে ভাবে, একটি ছোট মডেলে দেখি। নিচের Node.js কোড props-এর প্রতিটি ঘর হেঁটে দেখে, অনুমোদিত হলে লেখায় বদলায়, না হলে পথসহ error দেয়। এটি React-এর আসল কোড নয়; শুধু $D চিহ্নটি আসল payload-এর মতো, $F চিহ্নটি আমাদের নিজের বানানো।

// সীমানা-পাহারাদারের শিক্ষামূলক মডেল (React-এর আসল কোড নয়)
function encode(v, path) {
  if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) return v;
  if (v === undefined) return '$undefined';
  if (typeof v === 'function') {
    if (v.isServerFunction) return '$F' + v.id;          // শুধু 'use server' ফাংশনের ঠিকানা যায়
    throw new Error(`${path}: function পাঠানো যায় না`);
  }
  if (v instanceof Date) return '$D' + v.toISOString();
  if (Array.isArray(v)) return v.map((x, i) => encode(x, `${path}[${i}]`));
  const proto = Object.getPrototypeOf(v);
  if (proto !== Object.prototype) {
    const name = proto && proto.constructor ? proto.constructor.name : 'null prototype';
    throw new Error(`${path}: ${name} class-এর object পাঠানো যায় না`);
  }
  const out = {};
  for (const [k, x] of Object.entries(v)) out[k] = encode(x, `${path}.${k}`);
  return out;
}
class Taka { constructor(v) { this.v = v; } }
const addToCart = Object.assign(async () => {}, { isServerFunction: true, id: 'a1' });
const tries = {
  plain: { slug: 'jamdani', price: 4500 },
  date: { slug: 'jamdani', listedAt: new Date('2026-11-01T04:00:00Z') },
  action: { slug: 'jamdani', action: addToCart },
  handler: { slug: 'jamdani', onAdded: () => console.log('added') },
  money: { slug: 'jamdani', price: new Taka(4500) },
};
for (const [name, props] of Object.entries(tries)) {
  try {
    console.log('OK  ', name, JSON.stringify(encode(props, 'props')));
  } catch (e) {
    console.log('FAIL', name, e.message);
  }
}

আউটপুট:

OK   plain {"slug":"jamdani","price":4500}
OK   date {"slug":"jamdani","listedAt":"$D2026-11-01T04:00:00.000Z"}
OK   action {"slug":"jamdani","action":"$Fa1"}
FAIL handler props.onAdded: function পাঠানো যায় না
FAIL money props.price: Taka class-এর object পাঠানো যায় না

পাঁচটির মধ্যে তিনটি পার হলো। সাধারণ object সরাসরি গেল। Date গেল $D চিহ্ন নিয়ে। Server Function-এর কোড যায়নি, গেছে শুধু একটি ঠিকানা ($Fa1); ব্রাউজার সেটি ডাকলে আসলে সার্ভারে একটি অনুরোধ যায়, যা পর্ব ৫৪-এর (Server Action) বিষয়। সাধারণ function আর Taka object আটকে গেল, আর বার্তায় ঠিক কোন ঘরে সমস্যা তা লেখা আছে, আসল Next.js-এর মতোই।

কতটুকু পাঠাবেন: props-এর আকার

Serializable হলেই পাঠানো উচিত নয়। Database-এর পুরো সারি client-কে দিলে দুটি ক্ষতি। প্রথমত, প্রতিটি byte payload-এ যায়। দ্বিতীয়ত, payload যে কেউ নেটওয়ার্ক ট্যাবে পড়তে পারে, তাই costPrice বা সরবরাহকারীর ফোন নম্বরের মতো ঘর ফাঁস হয়। নিচে হিসাব করি।

// Client component-কে কতটুকু ডেটা দিচ্ছেন? UTF-8 byte গুনে দেখি
const bytes = (v) => new TextEncoder().encode(JSON.stringify(v)).length;
const reviews = Array.from({ length: 40 }, (_, i) => ({
  id: i + 1, by: 'ক্রেতা ' + (i + 1), stars: (i % 5) + 1,
  text: 'শাড়ির রং আর বুনন খুব ভালো লেগেছে। '.repeat(4),
}));
const product = {                           // database থেকে আসা পুরো সারি
  id: 101, slug: 'jamdani', name: 'জামদানি শাড়ি', price: 4500, stock: 3,
  description: 'হাতে বোনা ঢাকাই জামদানি। '.repeat(60),
  supplierPhone: '01700-000000', costPrice: 3100,   // গ্রাহকের দেখার কথা নয়
  images: ['/a.jpg', '/b.jpg', '/c.jpg'], reviews,
};
const pick = ({ slug, name, price, stock }) => ({ slug, name, price, stock });

const whole = bytes(product);
const picked = bytes(pick(product));
console.log('পুরো product prop :', whole, 'byte');
console.log('বাছাই করা prop    :', picked, 'byte');
console.log('অনুপাত            :', (whole / picked).toFixed(1) + ' গুণ');
const perDay = 10000;                       // ধরে নেওয়া দৈনিক পাতা দেখা
const mb = (n) => (n * perDay / 1024 / 1024).toFixed(1) + ' MB';
console.log(`দিনে ${perDay} বার: পুরো = ${mb(whole)}, বাছাই = ${mb(picked)}`);
console.log('অকারণে যাওয়া ঘর   :', Object.keys(product).filter((k) => !(k in pick(product))).join(', '));

আউটপুট:

পুরো product prop : 21134 byte
বাছাই করা prop    : 88 byte
অনুপাত            : 240.2 গুণ
দিনে 10000 বার: পুরো = 201.5 MB, বাছাই = 0.8 MB
অকারণে যাওয়া ঘর   : id, description, supplierPhone, costPrice, images, reviews

AddToCart-এর দরকার মাত্র চারটি ঘর, ৮৮ byte. পুরো সারি দিলে ২১,১৩৪ byte, প্রায় ২৪০ গুণ। দিনে ধরে নেওয়া ১০,০০০ বার দেখায় সেটি ২০১.৫ MB বনাম ০.৮ MB (gzip-এর আগে)। তালিকার supplierPhone আর costPrice শুধু অপচয় নয়, তথ্য ফাঁস। নিয়ম তাই সহজ: client-কে শুধু সেই ঘর দিন, যা সে দেখায় বা ব্যবহার করে।

৫ · children আর slot: client-এর ভেতরে server-এর জিনিস

অংশ ৩-এর সতর্কবার্তা মনে করুন: Client Component server-এর ফাইল import করতে পারে না। তাহলে Gallery-র (client) ভেতরে দামের অংশটি (server-এ হিসাব করা) কীভাবে বসল? উত্তর: Gallery দামের component import করেনি। ProductPage (server) সেটি তৈরি করে children হিসেবে দিয়েছে। পর্ব ০৯-এ children আর slot prop দিয়ে composition শিখেছিলেন; এখানে সেই কৌশলটিই সীমানা পার করার চাবি।

'use client';
import { useState } from 'react';

export default function Gallery({ images, children }) {
  const [i, setI] = useState(0);
  return (
    <section>
      <img src={images[i]} alt="" width={200} />
      {images.map((src, k) => (
        <button key={src} onClick={() => setI(k)}>{k + 1}</button>
      ))}
      {children}
    </section>
  );
}

React এখানে দুটি ভূমিকা আলাদা করে। Owner হলো যে component-এর কোডে JSX লেখা হয়েছে; দামের <p>-এর owner ProductPage. Parent হলো tree-তে যে বাইরে থেকে মুড়ে রাখে; সেটি Gallery. Owner server বলে অংশটি সার্ভারে তৈরি হয়। Gallery শুধু তৈরি ফলটি জায়গামতো বসায়, তার কোড কখনো দেখে না। দর্জির দোকানের মতো: দর্জি কাপড় কাটেন আর জোড়া দেন, কিন্তু কাপড়টি তাঁতে বোনা হয়েছে অন্য কোথাও।

ব্রাউজারে Next.js চালানো যায় না, তাই নিচের live React block একটি শিক্ষামূলক মডেল। PriceBox-কে আমরা component হিসেবে না বসিয়ে আগে থেকে একবার ডেকে ফল (JSX element) বানিয়ে রাখি, যেভাবে সার্ভার করে। তারপর client দ্বীপ Gallery সেটি children হিসেবে পায়। লোড হওয়ার পর কোড নিজেই দুবার ছবি বদলায়।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let serverRuns = 0, galleryRuns = 0, pick;
function PriceBox({ price }) {          // "server" অংশ: একবারই চলবে
  serverRuns++;
  return <p>দাম: ৳{price.toLocaleString('bn-BD')}</p>;
}
function Gallery({ images, children }) {   // client দ্বীপ
  const [i, setI] = useState(0);
  pick = setI;
  galleryRuns++;
  useEffect(() => {
    console.log(`ছবি ${i + 1} · Gallery চলল ${galleryRuns} বার · PriceBox চলল ${serverRuns} বার`);
  });
  return <section><b>{images[i]}</b>{children}</section>;
}
// "server" আগে থেকে PriceBox-এর ফল বানিয়ে রাখল; client শুধু তৈরি element পায়
const madeOnServer = PriceBox({ price: 4500 });
createRoot(document.getElementById('root')).render(
  <Gallery images={['a.jpg', 'b.jpg', 'c.jpg']}>{madeOnServer}</Gallery>
);
setTimeout(() => {
  for (const k of [1, 2]) ReactDOM.flushSync(() => pick(k));
}, 50);
</script>

আউটপুট:

ছবি 1 · Gallery চলল 1 বার · PriceBox চলল 1 বার
ছবি 2 · Gallery চলল 2 বার · PriceBox চলল 1 বার
ছবি 3 · Gallery চলল 3 বার · PriceBox চলল 1 বার

Gallery তিনবার render হলো (প্রথমবার আর দুটি ছবি বদলে), কিন্তু PriceBox চলল মাত্র একবার। কারণ children একটি তৈরি element, function নয়; state বদলালে React সেটি আবার বানায় না, শুধু জায়গামতো বসায়। আসল Next.js-এ ব্যাপারটি আরও কঠোর: server অংশের কোড ব্রাউজারে থাকেই না, তাই চাইলেও আবার চালানো যায় না। দাম বদলাতে হলে সার্ভারে নতুন করে render করে নতুন payload আনতে হয়।

শুধু children নয়, যেকোনো prop-এ JSX দেওয়া যায়। ধরুন একটি client Tabs-এ তিনটি ট্যাব: “বিবরণ”, “মাপ”, “রিভিউ”। প্রতিটি ট্যাবের ভেতরের লেখা server-এ তৈরি করে description={<Description />}, reviews={<Reviews />} হিসেবে পাঠান। ট্যাব বদলানো client-এ, ভেতরের ভারী লেখা server-এ। একটি সতর্কতা: Menu.Item-এর মতো static property-ওয়ালা compound component সীমানার ওপারে ভাঙে, কারণ server দিক থেকে client module-এর জায়গায় শুধু একটি client reference পাওয়া যায়, তার property নয়। Next.js-এর ডকুমেন্টেশন বলে এক্ষেত্রে “Element type is invalid” error আসে; সমাধান হলো named export ব্যবহার করা।

৬ · RSC payload: নেটওয়ার্ক ট্যাবে আসল flight data

Server Component-এর ফল ব্রাউজারে কী রূপে যায়? HTML হিসেবে, আবার একটি বিশেষ লেখা হিসেবেও। সেই লেখাটির নাম React Server Component Payload, সংক্ষেপে RSC payload; React-এর ভেতরের ভাষায় একে “Flight” বলা হয়। Next.js ডকুমেন্টেশন অনুযায়ী এতে থাকে: Server Component-এর render করা ফল, Client Component কোথায় বসবে তার জায়গা আর তাদের JS ফাইলের ঠিকানা, আর server থেকে client-কে দেওয়া props.

Chrome বা Edge-এর DevTools-এ Network ট্যাব খুলে হোম পাতা থেকে পণ্যের লিংকে ক্লিক করুন। ?_rsc=… লেখা একটি অনুরোধ দেখবেন, যার উত্তরের ধরন text/x-component. একই জিনিস আমরা curl দিয়ে আনলাম, RSC: 1 header দিয়ে, যেভাবে router নিজে চায়। (বর্ণনা: DevTools-এর ট্যাবের নাম version অনুযায়ী বদলাতে পারে; নিচের উত্তরটি সত্যিকারের run থেকে।)

~/bazarghar$ curl -i -H 'RSC: 1' 'http://127.0.0.1:3852/products/jamdani?_rsc'
HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
Content-Type: text/x-component

1:"$Sreact.fragment"
2:"/_next/static/chunks/2re141h9bmujs.js"
3:I[36862,["$2"],"CartProvider"]
4:I[88103,["$2"],"default"]
…
15:"/_next/static/chunks/15jiyzyv9k8_z.js"
16:I[74471,["$2","$15"],"default"]
17:I[37958,["$2","$15"],"default"]
…
b:["$","main",null,{"children":[["$","h1",null,{"children":"জামদানি শাড়ি"}],
  ["$","$L16",null,{"images":["/a.jpg","/b.jpg","/c.jpg"],
     "children":["$","p",null,{"children":["দাম: ৳","৪,৫০০"]}]}],
  ["$","$L17",null,{"product":{"slug":"jamdani","name":"জামদানি শাড়ি","price":4500}}],
  ["$","ul",null,{"children":[["$","li","রুমানা",{"children":[["$","b",null,{"children":"রুমানা"}],
     ["$","div",null,{"dangerouslySetInnerHTML":{"__html":"<p><strong>খুব সুন্দর</strong> কাজ, রং <em>একদম</em> ছবির মতো।</p>\n"}}]]}],
  …]}]]}]

প্রথম দেখায় জট লাগে, কিন্তু ভাগ করে পড়লে সহজ। প্রতিটি লাইন id:মান আকারে, আর একটি লাইন আরেকটিকে $ দিয়ে উল্লেখ করে। চিহ্নগুলোর এই ব্যাখ্যা আমাদের পর্যবেক্ষণ থেকে; এটি React-এর ভেতরের, অঘোষিত format, version-এ বদলাতে পারে।

যা দেখলেনঅর্থ
16:I[74471,["$2","$15"],"default"]একটি Client Component-এর ঠিকানা: module id 74471, কোন JS chunk লাগবে, কোন export. এখানে Gallery.
["$","$L16",null,{…}]একটি element. type হিসেবে $L16, অর্থাৎ “১৬ নম্বর লাইনের client component এখানে বসবে”; পাশে তার props.
["$","h1",null,{…}]সাধারণ HTML element. Server Component-এর ফল এই রূপেই আসে; component-এর নাম বা কোড নেই।
"$D2026-…", "$undefined"JSON-এ যা নেই তার জন্য বিশেষ চিহ্ন: Date, undefined.
"$Sreact.suspense"React-এর নিজের বিশেষ type, যেমন Suspense বা Fragment.

সবচেয়ে গুরুত্বপূর্ণ পর্যবেক্ষণ: Reviews নামটি payload-এ কোথাও নেই। আছে শুধু তার ফল, ul, li আর তৈরি HTML. অর্থাৎ marked, sanitize-html বা review render-এর কোনো কোড ব্রাউজার পায়নি। অন্যদিকে Gallery আর AddToCart এসেছে ঠিকানা ($L16, $L17) আর props হিসেবে। দামের <p> বসেছে Gallery-র props-এর children ঘরে, ঠিক অংশ ৫-এর মতো।

প্রথমবার পাতা খুললে আলাদা অনুরোধ হয় না; payload HTML-এর ভেতরেই <script>self.__next_f.push(…)</script> আকারে আসে। আমাদের পাতার পুরো HTML ছিল ৭,৭৬২ byte; আলাদা করে আনা payload ৪,৭৮৮ byte. পরে লিংকে ক্লিক করে অন্য পাতায় গেলে router শুধু payload আনে (?_rsc), HTML নয়। নিচের ধাপগুলোতে প্রথম অনুরোধের পুরো যাত্রা দেখুন।

  1. অনুরোধGET /products/jamdaniসার্ভারে পৌঁছাল
  2. Server অংশawait getProduct()রান্নাঘরে রান্না
  3. RSC payload$L16, $L17ফল + ঠিকানা
  4. HTML<main>…client অংশও সার্ভারে
  5. ব্রাউজারপাতা দেখা যায়এখনো ক্লিক কাজ করে না
  6. HydrationGallery, AddToCartদ্বীপগুলো সচল
অনুরোধকোনো JS এখনো চলেনি
GET /products/jamdani HTTP/1.1
Host: 127.0.0.1:3852
সার্ভারের terminalServer Component চলছে
ProductPage render jamdani
// Reviews: marked.parse → sanitizeHtml, সার্ভারেই
RSC payload (অংশ)text/x-component
16:I[74471,["$2","$15"],"default"]
b:["$","main",null,{"children":[
  ["$","h1",null,{"children":"জামদানি শাড়ি"}],
  ["$","$L16",null,{"images":[…],"children":…}],
  ["$","$L17",null,{"product":{…}}], …
HTML (অংশ)AddToCart-ও সার্ভারে চলল
<main><h1>জামদানি শাড়ি</h1><section><img src="/a.jpg" …/>
<button>1</button>…<p>দাম: ৳<!-- -->৪,৫০০</p></section>
<button>ঝুড়িতে যোগ করুন</button><ul>…
ব্রাউজারHTML + ভেতরে payload
<script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n…"])</script>
// ৮টি script ফাইল নামছে; পাতার নিজস্ব chunk 692 byte
ব্রাউজারের consoleশুধু client অংশ আবার চলল
AddToCart render jamdani
// ProductPage-এর লাইন এখানে কখনো আসে না
  1. অনুরোধ এল। ক্রেতা সরাসরি /products/jamdani খুললেন। এটি প্রথম লোড, তাই সার্ভারকে HTML আর payload দুটিই দিতে হবে।
  2. Server অংশ চলল। ProductPage database থেকে পণ্য আনল, Reviews markdown-কে নিরাপদ HTML বানাল। এই কাজের কোড ও library সার্ভারেই থাকে। Terminal-এ ProductPage render jamdani.
  3. Payload তৈরি। Server অংশের ফল সাধারণ element হয়ে গেল। Gallery আর AddToCart-এর জায়গায় বসল $L16 আর $L17, সাথে তাদের serializable props.
  4. HTML তৈরি। Payload আর Client Component মিলিয়ে সার্ভার পুরো HTML বানাল। এজন্যই Client Component-ও সার্ভারে চলে; বোতামের লেখা “ঝুড়িতে যোগ করুন” প্রথম HTML-এই আছে, যা search engine-ও পড়তে পারে।
  5. ব্রাউজার দেখাল। HTML পৌঁছানোমাত্র পাতা দেখা যায়, কিন্তু বোতাম এখনো সাড়া দেয় না। Payload এসেছে HTML-এর ভেতরে self.__next_f.push হয়ে; আলাদা অনুরোধ লাগেনি।
  6. Hydration. JS নামার পর React শুধু client দ্বীপগুলো আবার চালিয়ে তৈরি HTML-এ event handler জুড়ে দিল। Console-এ AddToCart render jamdani. Server অংশের HTML ছোঁয়া হয়নি। Hydration-এর খরচ ও mismatch পর্ব ৫৬-এ।

৭ · Bundle-এর হিসাব: markdown library server-এ বনাম client-এ

অংশ ৩-এর stepper-এর সংখ্যাগুলো কোথা থেকে এল, এবার খোলাখুলি দেখাই। “বাজারঘর”-এ ক্রেতার review markdown-এ লেখা। দেখানোর আগে দুটি কাজ: marked (লেখার সময়ে 18.1.2) দিয়ে markdown থেকে HTML, তারপর sanitize-html (2.18.1) দিয়ে বিপজ্জনক tag বাদ দেওয়া। দ্বিতীয় ধাপটি বাদ দেওয়া যাবে না: আমাদের পরীক্ষার একটি review-তে ইচ্ছা করে <img src=x onerror=alert(1)> রাখা ছিল, আর sanitize করার পর HTML-এ সেটি পুরোপুরি বাদ গেছে। dangerouslySetInnerHTML-এ অপরিষ্কার HTML দেওয়া মানে XSS-এর দরজা খোলা (পর্ব ৫০, Cybersecurity কোর্স)।

import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';

export default function Reviews({ reviews }) {
  return (
    <ul>
      {reviews.map((r) => (
        <li key={r.by}>
          <b>{r.by}</b>
          <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(marked.parse(r.md)) }} />
        </li>
      ))}
    </ul>
  );
}

আমরা একই অ্যাপ দুবার next build করলাম। প্রথমবার reviews.js যেমন আছে (server), দ্বিতীয়বার শুধু প্রথম লাইনে 'use client'; যোগ করে। প্রতিবার next start চালিয়ে পাতার HTML-এ যত <script src> আছে, সব নামিয়ে byte গুনলাম, আর তুলনার জন্য নিজেরা gzip (level 9) করে মাপলাম। সার্ভার আসলে কতটা compress করে পাঠায়, সেটি সেটআপ অনুযায়ী আলাদা।

~/bazarghar$ npx next build
▲ Next.js 16.4.4 (Turbopack)
✓ Compiled successfully in 324ms
✓ Generating static pages using 5 workers (3/3) in 390ms

Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /products/[slug]

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

# নিজের মাপার script-এর ফল, দুই build
server { scripts: 8, rawKB: '557.5', gzipKB: '171.0', markedShipped: false }
client { scripts: 8, rawKB: '781.7', gzipKB: '247.8', markedShipped: true }
# পাতার নিজস্ব chunk
server: 15jiyzyv9k8_z.js      692 byte
client: 1_tzqrjh77t11.js  230,210 byte (gzip 79,126)

লক্ষ করুন, Next.js 16-এর build টেবিলে আগের মতো প্রতিটি route-এর “First Load JS” আকার দেখায় না; তাই আমরা নিজে মেপেছি। ƒ চিহ্ন মানে পাতাটি প্রতি অনুরোধে সার্ভারে render হয়, কারণ [slug] আগে থেকে জানা নেই। এই চিহ্নগুলোর বিস্তারিত পর্ব ৫৫-এ।

সতর্কতা: এগুলো এক মেশিনের এক build-এর মাপ, সাধারণ ফল নয়। ৮টি script-এর বেশিরভাগ (৫৫৭ KB-এর প্রায় পুরোটা) React, React DOM আর Next.js router-এর, যা প্রতিটি App Router পাতায় লাগে। আমাদের তুলনার বিষয় শুধু পার্থক্যটুকু।

হিসাব করে দেখি

পরীক্ষায় এমন প্রশ্ন আসতে পারে: “একটি library client-এ গেলে ক্রেতার কত সময় আর ডেটা খরচ হয়?” ধাপে ধাপে করি। সব ধরে নেওয়া মান স্পষ্ট লেখা।

  1. বাড়তি byte (gzip): ২৪৭.৮ − ১৭১.০ = ৭৬.৮ KB = ৭৬.৮ × ১০২৪ ≈ ৭৮,৬৪৩ byte.
  2. ধীর সংযোগ ধরে নিই ১ Mbps = ১০,০০,০০০ bit/s = ১,২৫,০০০ byte/s. নামাতে বাড়তি সময় = ৭৮,৬৪৩ ÷ ১,২৫,০০০ ≈ ০.৬৩ সেকেন্ড. এটি শুধু নামানো; এরপর ২২৪.২ KB (raw) JavaScript পড়া, compile আর চালানোর খরচ main thread-এ যোগ হয়, যা ধীর ফোনে আরও বেশি।
  3. দিনে ধরে নেওয়া ১০,০০০ জন নতুন দর্শক (cache ছাড়া): ৭৬.৮ KB × ১০,০০০ = ৭,৬৮,০০০ KB ÷ ১০২৪ = ৭৫০ MB বাড়তি ডেটা, প্রতিদিন। বাংলাদেশে অনেক ক্রেতা মোবাইল ডেটার প্যাকেজে চলেন; সেই খরচ তাঁদের পকেট থেকে যায়।
  4. বিনিময়ে কী পেলাম? কিছুই না। Review-এর লেখা প্রতিটি ক্রেতার জন্য একই, ক্লিকে বদলায় না। Server-এ একবার HTML বানালেই হয়।

উল্টো দিকটিও সৎভাবে দেখা দরকার। Server version-এ payload-এ markdown-এর বদলে তৈরি HTML যায়, যা সাধারণত markdown-এর চেয়ে কিছুটা লম্বা (**খুব সুন্দর** বনাম <strong>খুব সুন্দর</strong>). দুটি review-তে এই পার্থক্য কয়েক ডজন byte; ২২৪ KB-এর সামনে নগণ্য। তবে ১,০০০টি review একসাথে দেখালে payload বড় হবে; তখন pagination (পর্ব ৩৪) লাগবে, server বা client যেটাই হোক।

৮ · গোপন জিনিস পাহারা: server-only, client-only আর env

অংশ ৩-এর মডেলে দেখেছেন, ভুল "use client" হলে db.js-ও ব্রাউজারে যেতে পারে। Next.js-এর ডকুমেন্টেশন একে “environment poisoning” বলে। একটি ফাইল server আর client দুই দিক থেকেই import করা যায়, তাই কেউ ভুল করে গোপন ফাইলকে client-এ টেনে নিতে পারে। আমরা এটি সত্যিই ঘটিয়ে দেখলাম।

পরীক্ষাটি এমন: db.js থেকে import 'server-only' লাইনটি সরালাম, আর add-to-cart.js-এ (client) getProduct import করলাম, যেমনটি AI প্রায়ই প্রস্তাব করে (“বোতাম চাপলে stock নতুন করে দেখে নিই”)। তারপর build.

~/bazarghar$ npx next build
✓ Compiled successfully
# build সফল! এবার client chunk-এ secret খুঁজি
~/bazarghar$ grep -o ".\{60\}example-secret.\{10\}" .next/static/chunks/*.js
.next/static/chunks/1tyviqd11gk-i.js:(43476),i=t.i(36862);(t.i(47167)).default.env.DB_PASSWORD??"example-secret";t.s(["default",0,function({product:t})

Build কোনো আপত্তি করেনি, আর example-secret লেখাটি ব্রাউজারের ফাইলে পৌঁছে গেছে। যে কেউ DevTools-এর Sources ট্যাবে এটি পড়তে পারত। লক্ষ করুন, process.env.DB_PASSWORD-এর আসল মান যায়নি; Next.js শুধু NEXT_PUBLIC_ দিয়ে শুরু হওয়া environment variable client bundle-এ বসায়, বাকিগুলো ব্রাউজারে খালি থাকে। কিন্তু কোডে লেখা fallback মান বা hard-coded key সরাসরি চলে যায়। আর NEXT_PUBLIC_ দিয়ে কোনো secret রাখলে সেটি ইচ্ছা করেই সবার কাছে পাঠানো হয়।

এবার db.js-এর প্রথম লাইনে import 'server-only'; ফিরিয়ে আনলাম। server-only একটি ছোট চিহ্ন-package (লেখার সময়ে 0.0.1); Next.js-এ এটি install করা ঐচ্ছিক, কারণ Next.js নিজেই এই import চেনে আর পরিষ্কার error দেয়।

import 'server-only';                     // এই ফাইল client-এ ঢুকলে build থামবে

const DB_PASSWORD = process.env.DB_PASSWORD ?? 'example-secret';

const products = [
  { slug: 'jamdani', name: 'জামদানি শাড়ি', price: 4500, stock: 3,
    images: ['/a.jpg', '/b.jpg', '/c.jpg'],
    reviews: [ /* ক্রেতার লেখা markdown */ ] },
];

export async function getProduct(slug) {
  if (!DB_PASSWORD) throw new Error('no db');   // আসল অ্যাপে এখানে Prisma query
  return products.find((p) => p.slug === slug) ?? null;
}
~/bazarghar$ npx next build
▲ Next.js 16.4.4 (Turbopack)
  Creating an optimized production build ...

> Build error occurred
Error: Turbopack build failed with 2 errors:
./lib/db.js:1:1
Error: You're importing a module that depends on "server-only". This API is only available in Server Components in the App Router, but you are using it in the Pages Router.
…
./lib/db.js:1:1
Error: 'server-only' cannot be imported from a Client Component module
> 1 | import 'server-only';
    | ^^^^^^^^^^^^^^^^^^^^^
It should only be used from a Server Component.

Import traces:
  Server Component:
    ./lib/db.js
    ./app/products/[slug]/page.js

  Client Component Browser:
    ./lib/db.js [Client Component Browser]
    ./app/ui/add-to-cart.js [Client Component Browser]
    ./app/ui/add-to-cart.js [Server Component]
    ./app/products/[slug]/page.js [Server Component]

এবার build থেমেছে (exit code 1). দুটি জিনিস খেয়াল করুন। প্রথম বার্তাটি “Pages Router” বলছে, যদিও আমরা App Router-এ; এই version-এ এটি বিভ্রান্তিকর, আসল কথা দ্বিতীয় বার্তায়। আর “Import traces” অংশটি সোনার মতো দামি: কোন ফাইল কোন ফাইলকে import করে ভুলটি ঘটাল, পুরো পথ দেখায়। নিচের দিক থেকে পড়ুন: page.js (server) → add-to-cart.js (সীমানা) → db.js, যা এখন ব্রাউজারের দিকে।

1// lib/db.js2import 'server-only';      // ধাপ ৪-এ যোগ3const DB_PASSWORD = process.env.DB_PASSWORD ?? 'example-secret';4export async function getProduct(slug) { … }5// app/ui/add-to-cart.js6'use client';7import { getProduct } from '@/lib/db';
অবস্থা
db.js-এ server-only
next build
client chunk-এ secret
  1. ভুলের শুরু। একটি client ফাইল ('use client', লাইন ৬) database-এর ফাইল import করল (লাইন ৭)। এটি সীমানার ভেতরে, তাই db.js-ও client graph-এ ঢুকবে।
  2. পাহারা নেই, build সফল। db.js-এ server-only নেই। Bundler কোনো আপত্তি দেখল না; তার কাছে এটি আর দশটা ফাইলের মতোই।
  3. Secret ব্রাউজারে। লাইন ৩-এর fallback 'example-secret' client chunk 1tyviqd11gk-i.js-এ হুবহু বসে গেছে। আসল অ্যাপে এটি হতো একটি সত্যিকারের key.
  4. পাহারাদার বসানো। db.js-এর মাথায় import 'server-only' (লাইন ২)। এর মানে: “এই ফাইল শুধু server graph-এ থাকতে পারে।”
  5. Build থামল। একই ভুল import এখন 'server-only' cannot be imported from a Client Component module, exit code 1. Secret কোনো ফাইলে লেখা হওয়ার আগেই ধরা পড়ল। ভুলটি production-এ নয়, আপনার মেশিনেই।
  6. আসল সমাধান। লাইন ৭ মুছুন। Stock আগে থেকে server-এ পড়ে prop হিসেবে দিন, আর বোতাম চাপলে নতুন করে যাচাই দরকার হলে Server Action ব্যবহার করুন (পর্ব ৫৪)। Build আবার সফল, secret সার্ভারেই।

উল্টো দিকের পাহারাদার client-only. কোনো ফাইল যদি window বা localStorage-এর ওপর নির্ভর করে, তার মাথায় import 'client-only' দিলে কেউ সেটি Server Component থেকে import করলে build-এ error আসে। দুটিই চিহ্ন মাত্র; নিরাপত্তার একমাত্র ভরসা নয়। Secret সবসময় environment variable-এ রাখুন, কোডে নয় (পর্ব ৭৮), আর data-access ফাইলগুলো এক জায়গায় রেখে প্রতিটির মাথায় server-only দিন।

আরও গভীরে: server-only package-এর ভেতরে মাত্র দুটি ফাইল। সাধারণ entry একটি error ছোড়ে, আর package.json-এর exports-এ "react-server" condition-এর জন্য একটি খালি ফাইল দেওয়া আছে। RSC-সমর্থিত bundler server graph তৈরির সময় এই react-server condition চালু রাখে, তাই server দিকে খালি ফাইল আসে আর client দিকে error-ওয়ালা ফাইল। অর্থাৎ পাহারাটি Node.js-এর conditional exports-এর ওপর দাঁড়ানো, কোনো জাদু নয়। React আরও এক ধাপ এগিয়ে পরীক্ষামূলক “taint” API (experimental_taintObjectReference, experimental_taintUniqueValue) এনেছে, যা নির্দিষ্ট object বা মানকে client-এ যাওয়া থেকে render-এর সময় আটকায়; লেখার সময়ে এগুলো experimental.

৯ · তৃতীয় পক্ষের component, context আর “বাজারঘর”-এর সিদ্ধান্ত

তৃতীয় পক্ষের component-এর জন্য wrapper

npm-এর অনেক React package এখনো "use client" ছাড়া লেখা, কারণ সেগুলো RSC-এর আগের। ধরুন ছবি বড় করার জন্য একটি package acme-zoom, যার ভেতরে useState আছে (পরীক্ষার জন্য আমরা নিজে এমন একটি ছোট package বানিয়েছি)। Server Component থেকে সরাসরি import করে চালালে:

ProductPage render jamdani
⨯ TypeError: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component
 GET /products/jamdani 500 in 675ms

Package-এর ফাইলে আপনি হাত দিতে পারেন না। সমাধান একটি দুই লাইনের wrapper, যা নিজেই সীমানা হয়ে যায়। এরপর পাতা আবার 200 দিল।

'use client';
export { Zoom } from 'acme-zoom';

নিজের ফাইলে "use client" ভুলে গেলে বার্তা আরও আগে আসে, compile-এর সময়ে, আর ঠিক কোন import দোষী তা দেখায়:

⨯ ./app/ui/gallery.js:1:10
Error: You're importing a module that depends on `useState` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the `"use client"` directive.
> 1 | import { useState } from 'react';
    |          ^^^^^^^^

Context provider সবসময় client

Server Component-এ React context চলে না। কিন্তু ঝুড়ির মতো জিনিস পুরো অ্যাপে ভাগ করতে context দরকার (পর্ব ১৭)। উপায়: একটি ছোট client provider বানান, যা শুধু children নেয়, আর সেটি server layout-এ বসান।

'use client';
import { createContext, useContext, useState } from 'react';

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  return <CartContext value={{ items, setItems }}>{children}</CartContext>;
}
export function useCart() {
  return useContext(CartContext);
}
import { CartProvider } from './cart-provider';
import CartBadge from './ui/cart-badge';

export const metadata = { title: 'বাজারঘর' };

export default function RootLayout({ children }) {
  return (
    <html lang="bn">
      <body>
        <CartProvider>
          <header><b>বাজারঘর</b> <CartBadge /></header>
          {children}
        </CartProvider>
      </body>
    </html>
  );
}

Layout এখনো Server Component. CartProvider client, কিন্তু সে বাকি পাতা children হিসেবে পায়, import করে না। তাই provider-এর নিচে থাকা ProductPage বা Reviews server-ই থাকে। React 19-এ provider লেখা হয় <CartContext value={…}> দিয়ে; পুরোনো কোডে CartContext.Provider দেখবেন, সেটিও এখনো চলে। Next.js-এর পরামর্শ: provider যত গভীরে সম্ভব বসান, পুরো <html> মুড়ে নয়।

সাধারণ ভুলগুলো

পুরো পাতা client

একটি বোতামের জন্য page.js-এর মাথায় "use client". তখন async/await দিয়ে ডেটা আনা যায় না, সব import bundle-এ যায়, আর secret ফাঁসের ঝুঁকি।

ওষুধ: শুধু বোতামটি আলাদা ফাইলে

Client থেকে server import

Client ফাইলে server component import করলে সেটিও client হয়ে যায়, আর তার async বা database কোড ভাঙে।

ওষুধ: children বা slot prop

পুরো DB সারি prop

product={p} লিখে সব ঘর পাঠানো। Payload বড়, আর costPrice-এর মতো ঘর সবাই দেখে।

ওষুধ: দরকারি ঘর বেছে দিন

অকারণে client fetch

Client component-এ useEffect দিয়ে নিজের API ডেকে ডেটা আনা, যা server-এ সরাসরি পড়া যেত। বাড়তি round trip আর loading spinner.

ওষুধ: server-এ await, prop দিন

“বাজারঘর”-এর পণ্য পাতা: কোন অংশ কোথায়

প্রতিটি অংশের জন্য একটি প্রশ্ন করুন: এই অংশ কি ক্রেতার কাজে সঙ্গে সঙ্গে বদলায়, বা ব্রাউজারের কিছু চায়? হ্যাঁ হলে client, না হলে server. তারপর client অংশকে যত ছোট সম্ভব করুন।

অংশকোথায়কারণ
নাম, বিবরণ, দামServerDatabase থেকে পড়া, ক্লিকে বদলায় না, SEO-র জন্য HTML-এ দরকার
ছবির gallery (thumbnail চাপলে বড় ছবি বদলায়)Client দ্বীপuseState আর onClick; ক্যাপশন বা দাম server থেকে children হিসেবে
“ঝুড়িতে যোগ করুন”Client দ্বীপContext-এর ঝুড়ি বদলায়; stock-এর চূড়ান্ত যাচাই সার্ভারে (পর্ব ৫৪)
Review তালিকা (markdown)Serverভারী parser আর sanitizer সার্ভারে থাকে; লেখা সবার জন্য এক
Review-এর “সহায়ক” বোতামছোট client দ্বীপশুধু বোতামটি; তালিকা server-ই থাকে
“একই রকম পণ্য” তালিকাServerDatabase query; ধীর হলে Suspense দিয়ে stream (পর্ব ৫৩)
মাপ বেছে নেওয়ার dropdownClient দ্বীপনির্বাচন state-এ; অথবা URL search param রেখে server-এ
ঝুড়ির ব্যাজ (header-এ সংখ্যা)Client দ্বীপContext পড়ে; provider layout-এ

একটি সূক্ষ্ম কথা: সব interactivity-র জন্য Client Component লাগে না। <details> নিজেই খোলে-বন্ধ হয়, <form> Server Function দিয়ে জমা দেওয়া যায়, <video controls> নিজেই চলে। ব্রাউজার যা নিজে পারে, তার জন্য JavaScript পাঠানোর দরকার নেই।

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

  • Server Component আর SSR কি একই? না। SSR বলে HTML কোথায় তৈরি; Server Component বলে কোড ব্রাউজারে যায় কি না। Client Component-ও SSR হয়।
  • "use client" কোথায় লাগে? শুধু client subtree-এর প্রবেশপথের ফাইলে; সেটির সব import নিজে থেকেই client.
  • Server থেকে client-কে function দেওয়া যায়? সাধারণ function নয়; শুধু "use server" চিহ্নিত Server Function, যা ঠিকানা হিসেবে যায়।
  • Client-এর ভেতরে server অংশ কীভাবে? children বা অন্য JSX prop দিয়ে; import করে নয়।
  • RSC payload কী? Server tree-এর serialize করা ফল, client component-এর ঠিকানা আর তাদের props; প্রথম লোডে HTML-এর ভেতরে, পরের navigation-এ আলাদা অনুরোধে আসে।

১০ · AI-এর যুগে: সীমানার ভুল AI প্রায়ই করে

AI সহকারীরা Next.js-এর কোড দ্রুত লেখে, কিন্তু server-client সীমানায় তাদের ভুল চোখে পড়ার মতো। কারণ ইন্টারনেটের বেশিরভাগ পুরোনো উদাহরণ Pages Router বা Next.js 13-14-এর। Next.js 16 নিজেও এটা মানে: আমাদের next dev চালাতেই project-এ একটি AGENTS.md ফাইল তৈরি হলো, যার প্রথম কথা “This is NOT the Next.js you know”, অর্থাৎ AI-কে আগে node_modules/next/dist/docs/-এর docs পড়তে বলা হয়েছে।

AI-এর ভুলযা দেখবেনঠিক পথ
সমস্যা হলেই page.js-এর মাথায় "use client"Error চলে যায়, কিন্তু bundle বড় হয়, ডেটা আনতে useEffectশুধু interactive অংশ আলাদা ফাইলে
getServerSideProps বা pages/ ফোল্ডারের কোডApp Router-এ কিছুই হয় না বা errorServer Component-এ সরাসরি await
params.slug সরাসরি পড়াNext.js 16-এ params promiseconst { slug } = await params
Client component-এ database বা env importBuild সফল, secret ফাঁসData ফাইলে import 'server-only'
onClick server থেকে prop হিসেবেEvent handlers cannot be passed…Handler client ফাইলের ভেতরে রাখুন
Error ঢাকতে try/catch দিয়ে চুপ করানোপাতা চলে, কিন্তু ভুল ডেটাError পড়ুন, import trace অনুসরণ করুন

AI-কে কাজ দেওয়ার সময় version আর সীমানার নিয়ম আগেই বলে দিন, আর তাকে ব্যাখ্যা দিতে বলুন কোন ফাইল কেন client:

আমি Next.js 16 (App Router) আর React 19 ব্যবহার করছি। একটি পণ্য পাতা বানাতে চাই: ছবির gallery, "ঝুড়িতে যোগ করুন" বোতাম, আর markdown review. নিয়ম: ১. page.js Server Component থাকবে, async/await দিয়ে ডেটা আনবে। Pages Router বা getServerSideProps ব্যবহার করবে না। ২. "use client" শুধু সেই ফাইলে দাও যেখানে state, effect বা event handler আছে। প্রতিটি "use client" ফাইলের পাশে এক লাইনে লিখে দাও কেন সেটি client. ৩. Client component-কে শুধু দরকারি ঘর prop হিসেবে দাও, পুরো database সারি নয়। ৪. Database ফাইলের মাথায় import 'server-only' রাখো। কোনো secret কোডে লিখবে না। ৫. নতুন npm package যোগ করার আগে জিজ্ঞেস করো।

AI-এর লেখা Next.js কোড পেলে: যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Server Components কি আসলে PHP-র যুগে ফিরে যাওয়া? তাহলে মাঝের দশ বছরের SPA কি ভুল ছিল?

প্রশ্নটি অনেকেই ঠাট্টা করে করেন, আর এর ভেতরে সত্যের একটি অংশ আছে। ১৯৯০-এর দশকের মাঝামাঝি থেকে PHP, পরে Rails ও Django-র মতো framework সার্ভারে HTML বানিয়ে পাঠাত। ডেটা আর secret সার্ভারেই থাকত, ব্রাউজারে যেত শুধু তৈরি পাতা। ঠিক যেমন আজ Server Component করে।

কিন্তু সেই মডেলে interactivity ছিল জোড়াতালি। সার্ভার HTML বানাত এক ভাষায়, আর ব্রাউজারে jQuery দিয়ে আলাদা কোডে সেটি জীবন্ত করা হতো। দুটি জায়গায় একই UI-এর দুই রকম বর্ণনা, যা সহজেই অমিল হতো। ২০০৫-এর দিকে AJAX জনপ্রিয় হলো, ২০১৩ সালে React এল, আর UI হয়ে উঠল একটি component tree, যা পুরোটাই ব্রাউজারে চলে। এতে মডেল সরল হলো, কিন্তু দাম দিতে হলো: সব component-এর কোড ব্রাউজারে, ডেটার জন্য আলাদা API, প্রথম লোডে ফাঁকা পাতা আর loading spinner-এর জলপ্রপাত।

RSC দুটির মাঝখানে একটি নতুন জিনিস করে: একটিই component মডেল, যা দুই পরিবেশে ভাগ হয়। Server অংশ আর client অংশ একই ভাষায়, একই JSX-এ, একই tree-তে লেখা, আর সীমানা পার হয় type-সচেতন props দিয়ে। আরেকটি বড় পার্থক্য: সার্ভার নতুন payload পাঠালে React সেটি বিদ্যমান tree-র সাথে reconcile করে, ফলে client অংশের state (যেমন ঝুড়ির input-এ লেখা) হারায় না। PHP-র যুগে নতুন HTML মানে পুরো পাতা নতুন করে আঁকা।

তাই SPA ভুল ছিল না; সে একটি সমস্যার (interactivity) সমাধান দিয়েছিল আরেকটি সমস্যা (bundle আর data waterfall) তৈরি করে। RSC দ্বিতীয়টি কমানোর চেষ্টা। তবে এটিও বিনামূল্যে নয়: সার্ভার লাগে, মানসিক মডেল জটিল, আর সীমানার ভুল (এই পর্বে যেমন দেখলেন) নতুন ধরনের bug আনে।

মূল উপলব্ধি: প্রযুক্তি প্রায়ই চক্রাকারে ফেরে, কিন্তু আগের জায়গায় নয়, এক ধাপ ওপরে। “পুরোনো জিনিস নতুন নামে” বলে উড়িয়ে না দিয়ে জিজ্ঞেস করুন: এবার কোন পুরোনো সমস্যা সমাধান হলো, আর কোন নতুন দাম দিতে হচ্ছে?

প্র ০২Props serializable হতেই হবে কেন? React কি function-ও পাঠানোর ব্যবস্থা করতে পারত না?

মূল কারণ একটি ভৌত সত্য: Server Component আর Client Component দুটি আলাদা কম্পিউটারে, আলাদা সময়ে চলে। মাঝখানে নেটওয়ার্ক। এক কম্পিউটারের memory-র কোনো জিনিস অন্যটিতে নিতে হলে সেটি byte-এ লিখতে হয়। একটি string বা number লেখা সহজ। কিন্তু একটি function কী? তার কোড, আর তার closure: যেসব ভ্যারিয়েবল সে চারপাশ থেকে ধরে রেখেছে, যেমন database connection, request-এর cookie, বা কোনো secret. সেগুলো ব্রাউজারে পাঠানো অসম্ভব, আর সম্ভব হলেও বিপজ্জনক।

এই সমস্যা নতুন নয়। ১৯৮০-এর দশক থেকে distributed system-এ RPC (remote procedure call) একই প্রশ্নের মুখোমুখি হয়েছে, আর Java RMI বা CORBA-র মতো প্রযুক্তি দূরের object-কে “স্থানীয় মনে হওয়ার” চেষ্টা করে অনেক জটিলতা তৈরি করেছিল। ১৯৯৪ সালের একটি বিখ্যাত প্রবন্ধ “A Note on Distributed Computing” (Waldo ও সহকর্মীরা, Sun Microsystems) যুক্তি দিয়েছিল যে দূরের কাজকে স্থানীয় কাজের মতো ভান করানো বিপজ্জনক, কারণ latency, আংশিক ব্যর্থতা আর memory-র পার্থক্য লুকানো যায় না।

React একটি মাঝামাঝি পথ নিয়েছে। সাধারণ function যায় না। কিন্তু "use server" চিহ্নিত Server Function যায়, তবে কোড হিসেবে নয়, একটি ঠিকানা হিসেবে। Client সেটি ডাকলে আসলে একটি HTTP অনুরোধ হয়। অর্থাৎ React সীমানাটিকে লুকায়নি, স্পষ্ট চিহ্ন দিয়ে দৃশ্যমান রেখেছে। তেমনি Date, Map, Set, Promise-এর মতো built-in জিনিস চেনে, কারণ সেগুলো ভেতরের অবস্থা সহ নির্ভরযোগ্যভাবে আবার বানানো যায়। নিজের class-এর object যায় না, কারণ ওপারে সেই class-এর কোড (method) না-ও থাকতে পারে; থাকলেও constructor-এর গোপন নিয়ম ভাঙতে পারে।

এর একটি নিরাপত্তা-দিকও আছে। ২০২৫ সালের ডিসেম্বরে React দল একটি সর্বোচ্চ মাত্রার দুর্বলতা (CVE-2025-55182) প্রকাশ করে: Server Function-এর endpoint-এ আসা payload অনিরাপদভাবে decode হতো, আর একটি বিশেষভাবে বানানো অনুরোধে সার্ভারে কোড চালানো সম্ভব ছিল। এটি দেখায়, serialization-এর সীমানা শুধু সুবিধার নয়, আক্রমণেরও জায়গা; যত কম জিনিস পার হয়, তত ভালো। React-এর advisory অনুযায়ী মূল সমাধান ছিল দ্রুত patch করা version-এ আপডেট করা।

মূল উপলব্ধি: নেটওয়ার্কের সীমানা কখনো অদৃশ্য নয়। ভালো নকশা সেটিকে লুকায় না, বরং স্পষ্ট করে, আর ঠিক করে দেয় কী পার হবে।

প্র ০৩“use client” নামটি কি বিভ্রান্তিকর? Islands architecture-এর সাথে এর মিল ও পার্থক্য কী?

নামটি অনেককে বিভ্রান্ত করে, দুই কারণে। প্রথমত, “client” শুনে মনে হয় component শুধু ব্রাউজারে চলে, অথচ সে সার্ভারেও HTML বানাতে চলে (অংশ ২-এ terminal-এ AddToCart render দেখেছেন)। দ্বিতীয়ত, মনে হয় এটি একটি component-এর লেবেল, অথচ এটি module graph-এ একটি দরজা; যা import হয় সবই ভেতরে যায়। React-এর ডকুমেন্টেশন নিজেই জোর দিয়ে বলে যে সীমানাটি module dependency tree-তে, render tree-তে নয়। অনেকে বলেন “use client” না বলে “এখান থেকে browser-এও পাঠাও” বললে বেশি সঠিক হতো, কিন্তু ছোট নামের সুবিধাও আছে।

এবার islands architecture. ধারণাটি ২০১৯ সালের দিকে Etsy-র প্রকৌশলী Katie Sylor-Miller প্রস্তাব করেন, আর ২০২০ সালে Preact-এর স্রষ্টা Jason Miller একটি লেখায় একে জনপ্রিয় করেন। মূল কথা: পাতার বেশিরভাগ static HTML-এর “সমুদ্র”, তার মাঝে কিছু interactive “দ্বীপ”, যেগুলো আলাদাভাবে hydrate হয়। Astro-র মতো framework এটিকে কেন্দ্রীয় নকশা বানিয়েছে। Islands architecture-এর সাথে RSC-এর মিল স্পষ্ট: দুটোই বলে, যে অংশ interactive নয় তার JS পাঠিও না।

পার্থক্যগুলোও গুরুত্বপূর্ণ। Islands-এ দ্বীপগুলো সাধারণত একে অপরের থেকে আলাদা, প্রায়ই আলাদা framework-এ লেখা যায়, আর তাদের মধ্যে state ভাগ করা কঠিন। RSC-এ পুরোটা একটি React tree: একটি client provider-এর নিচে server অংশ, তার নিচে আবার client দ্বীপ, সবাই একই context দেখে। আরেকটি পার্থক্য navigation-এ: RSC-এ লিংকে ক্লিক করলে নতুন payload আসে আর React বিদ্যমান tree-র সাথে মেলায়, client state বজায় থাকে। Islands-ভিত্তিক সাইট প্রায়ই প্রতিটি পাতা নতুন করে লোড করে (MPA).

কোনটি ভালো? বিষয়বস্তু-প্রধান সাইটে (blog, docs) islands-এর সরলতা প্রায়ই জেতে। অ্যাপ-এর মতো সাইটে, যেখানে অনেক ভাগ করা state আর ঘন navigation, RSC-এর একক tree সুবিধা দেয়। সম্প্রদায়ে এ নিয়ে মতভেদ আছে, আর দুই পক্ষেই যুক্তি আছে।

মূল উপলব্ধি: নাম মুখস্থ নয়, আচরণ বুঝুন। কোন কোড কোথায় চলে আর কী ব্রাউজারে যায়, সেটি build output আর network ট্যাব দেখে নিজে যাচাই করুন।

প্র ০৪Server Component-এ database সরাসরি পড়া যায়। এতে কি নিরাপত্তা বাড়ল, না নতুন ঝুঁকি এল?

দুটোই। আগে React অ্যাপে database পড়তে একটি API endpoint লাগত, যেমন /api/products/:slug. সেই endpoint ইন্টারনেটে খোলা, তাই সেখানে authentication, authorization, rate limit সব আলাদা করে ভাবতে হতো, আর প্রায়ই endpoint প্রয়োজনের চেয়ে বেশি ঘর ফেরত দিত। Server Component-এ সেই endpoint লাগে না; component সার্ভারে বসেই query চালায়। আক্রমণের একটি দরজা কমল, আর secret কখনো ব্রাউজারের কাছ দিয়েও যায় না।

কিন্তু নতুন ঝুঁকিও আছে, আর সেগুলো সূক্ষ্ম। প্রথমত, সীমানা অদৃশ্য হয়ে যায়। একই ফোল্ডারে, একই ভাষায় server আর client ফাইল পাশাপাশি; একটি import-এর ভুলে database-এর ফাইল client-এ চলে যেতে পারে, আর এই পর্বে দেখেছেন build তা না থামিয়েই সফল হতে পারে। দ্বিতীয়ত, props হলো একটি অদৃশ্য API. product={p} লিখলে পুরো সারি payload-এ যায়, যা যে কেউ পড়তে পারে। Next.js-এর ডকুমেন্টেশন এজন্যই একটি “Data Access Layer” রাখার পরামর্শ দেয়: একটি server-only ফাইলে সব database কাজ, যেখানে প্রতিটি ফাংশন নিজেই authorization যাচাই করে আর শুধু নিরাপদ ঘর (DTO) ফেরত দেয়।

তৃতীয়ত, Server Function. সেগুলো দেখতে সাধারণ ফাংশন, কিন্তু আসলে public HTTP endpoint. যে কেউ সেটি সরাসরি ডাকতে পারে, তাই ভেতরে input যাচাই আর অনুমতি পরীক্ষা বাধ্যতামূলক (পর্ব ৫৪)। চতুর্থত, framework-এর নিজের দুর্বলতা। ২০২৫ সালের ডিসেম্বরে RSC-এর payload decode করার কোডে সর্বোচ্চ মাত্রার একটি ত্রুটি (CVE-2025-55182) ধরা পড়ে, যা Server Function না লিখলেও RSC-সমর্থিত অ্যাপকে প্রভাবিত করত। পরের কয়েক সপ্তাহে একই এলাকায় আরও কয়েকটি ছোট ত্রুটি প্রকাশ পায়। নতুন প্রযুক্তি মানে নতুন আক্রমণের জায়গা; dependency দ্রুত আপডেট করা তাই নিরাপত্তার অংশ (পর্ব ৫০)।

মূল উপলব্ধি: RSC নিরাপত্তাকে সহজ করে না, শুধু জায়গা বদলায়। API endpoint-এর দেয়ালের বদলে এখন আপনাকে import, props আর Server Function, এই তিন সীমানা পাহারা দিতে হয়।

প্র ০৫সব React অ্যাপে কি Server Components ব্যবহার করা উচিত? কখন সাধারণ SPA-ই ভালো?

না, আর এ নিয়ে সম্প্রদায়ে সত্যিকারের মতভেদ আছে। RSC-এর সুবিধা সবচেয়ে বেশি যেখানে পাতায় অনেক পড়ার মতো বিষয়বস্তু, ডেটা সার্ভারের কাছে, আর ক্রেতারা ধীর ফোন বা সংযোগে: e-commerce, সংবাদ, docs, marketing সাইট। “বাজারঘর”-এর পণ্য পাতা এর আদর্শ উদাহরণ; SEO দরকার, প্রথম লোড দ্রুত হওয়া দরকার, আর বেশিরভাগ অংশ interactive নয়।

কিন্তু উল্টো ক্ষেত্রও আছে। একটি admin dashboard ভাবুন, যেখানে প্রতিটি অংশ ফিল্টার, drag, chart আর live update-এ ভরা, আর ব্যবহারকারী login করে ঘণ্টার পর ঘণ্টা থাকেন। সেখানে প্রায় সবই client; RSC-এর সুবিধা কম, কিন্তু জটিলতা পুরোটাই থাকে। Offline-এ চলা অ্যাপ, বা Figma-র মতো canvas-ভিত্তিক টুলে সার্ভারে render করার মতো খুব কিছু নেই। আবার RSC চালাতে একটি Node.js (বা সমতুল্য) সার্ভার লাগে; শুধু static ফাইল রাখা যায় এমন সাধারণ hosting-এ (অনেক shared hosting-এ) সেটি সম্ভব না-ও হতে পারে, যদিও build-এর সময় static HTML তৈরি করা কিছু পাতার জন্য সম্ভব।

সমালোচকেরা আরও কিছু কথা বলেন। মানসিক মডেল কঠিন হয়েছে: একজন নতুন শিক্ষার্থীকে এখন useState-এর আগে বুঝতে হয় কোন ফাইল কোথায় চলে। Debug করা কঠিন, কারণ একটি পাতার কোড দুই পরিবেশে ছড়ানো। আর প্রথম দিকে RSC কার্যত একটি framework-এর সাথে বাঁধা ছিল, যা vendor-নির্ভরতার উদ্বেগ তৈরি করেছিল; পরে React Router, Waku, Parcel ও Vite-এর plugin সমর্থন আনায় সেই উদ্বেগ কিছুটা কমেছে। অন্যদিকে সমর্থকেরা বলেন, bundle কমানো আর data waterfall দূর করার এমন পরিষ্কার উপায় আগে ছিল না।

একটি বাস্তব কৌশল হলো দুটো মেশানো: পাবলিক পাতাগুলো (হোম, পণ্য, ক্যাটাগরি) RSC-এ, আর login-এর পরের ভারী অংশ (বিক্রেতার dashboard) একটি client-প্রধান অংশে, যেখানে পুরো subtree-র মাথায় একটি "use client" সীমানা। পর্ব ৮৫-এ stack বাছাইয়ের ছক এই প্রশ্নগুলোই ধরে এগোবে।

মূল উপলব্ধি: RSC একটি টুল, ধর্ম নয়। আপনার ব্যবহারকারী কোথায়, কোন যন্ত্রে, কী করেন, আর আপনার hosting কী দেয়, সেটি দেখে সিদ্ধান্ত নিন; আর সিদ্ধান্তের আগে-পরে মাপুন।

অনুশীলন

হাতে হিসাব, ফল অনুমান আর নকশা যাচাই; নিজে চেষ্টা করে তারপর উত্তর দেখুন।

  1. হিসাব: একটি তারিখ-ফরম্যাটের library client-এ গেলে পাতার JS gzip-এ ৩২ KB বাড়ে (ধরে নেওয়া)। সংযোগ ধরে নিন ৫১২ kbps. (ক) নামাতে বাড়তি কত সেকেন্ড? (খ) দিনে ২৫,০০০ জন নতুন দর্শক হলে মাসে (৩০ দিন) কত GB বাড়তি ডেটা যায়? (১ KB = ১০২৪ byte, ১ GB = ১০২৪ MB ধরুন।)

    (ক) ৩২ KB = ৩২ × ১০২৪ = ৩২,৭৬৮ byte = ২,৬২,১৪৪ bit. ৫১২ kbps = ৫,১২,০০০ bit/s. সময় = ২,৬২,১৪৪ ÷ ৫,১২,০০০ ≈ ০.৫১ সেকেন্ড, শুধু নামাতে; চালানোর খরচ আলাদা।

    (খ) দিনে ৩২ × ২৫,০০০ = ৮,০০,০০০ KB; মাসে × ৩০ = ২,৪০,০০,০০০ KB. ÷ ১০২৪ = ২৩,৪৩৭.৫ MB; ÷ ১০২৪ ≈ ২২.৯ GB. যদি তারিখটি শুধু দেখানোর জন্য হয়, server-এ ফরম্যাট করে string পাঠালে পুরো খরচ বাঁচে।

  2. ফল অনুমান করুন: একটি Server Component থেকে Client Component-কে এই props দেওয়া হলো: (ক) { when: new Date('2026-01-01') } (খ) { tags: new Set(['শাড়ি']) } (গ) { onBuy: () => {} } (ঘ) { price: new Decimal('4500.00') }, যেখানে Decimal একটি library-র class. কোনগুলো চলবে, কোনগুলো error দেবে?

    (ক) চলবে। Date React-এর তালিকায় আছে; payload-এ $D… হয়ে যায়, ওপারে আবার Date. (খ) চলবে। ভেতরের মান serializable হলে Set অনুমোদিত। (গ) Error: Event handlers cannot be passed to Client Component props. নাম on দিয়ে শুরু বলে এই বার্তা। (ঘ) Error: Only plain objects, and a few built-ins, can be passed…, কারণ এটি একটি class-এর object. সমাধান: server-এ price.toString() বা number-এ রূপান্তর করে দিন।

  3. নকশা সমালোচনা: একজন লিখেছেন: app/products/[slug]/page.js-এর প্রথম লাইনে "use client", তারপর useEffect-এ fetch('/api/products/' + slug), আর ভেতরে gallery, ঝুড়ির বোতাম আর review. এই নকশার সমস্যা কী, আর কীভাবে নতুন করে সাজাবেন?

    সমস্যা: (১) প্রথম HTML-এ পণ্যের তথ্য নেই, শুধু loading, তাই SEO আর প্রথম দেখা দুটোই দুর্বল। (২) পাতা আসে, তারপর JS, তারপর fetch: একটি অকারণ round trip. (৩) Review-এর markdown library সহ সব কোড bundle-এ। (৪) /api/products একটি বাড়তি public endpoint, যা আলাদা পাহারা চায়।

    নতুন সাজ: page.js Server Component, await getProduct(slug). Gallery আর AddToCart আলাদা client ফাইল, শুধু দরকারি props পায়। Reviews server-এ markdown থেকে sanitize করা HTML বানায়। Database ফাইলে import 'server-only'. ঝুড়ির context provider layout-এ।

  4. Build ভেঙে দেখুন: নিজের একটি Next.js 16 project-এ lib/secret.js বানান, মাথায় import 'server-only', আর একটি "use client" component থেকে সেটি import করুন। npx next build চালান। Import trace-এ কোন তিনটি ফাইল দেখবেন, কোন ক্রমে? তারপর server-only লাইন সরিয়ে build করে .next/static/chunks-এ secret-এর লেখা খুঁজুন।

    প্রথম build ব্যর্থ হবে, বার্তা: 'server-only' cannot be imported from a Client Component module. “Client Component Browser” অংশে ওপর থেকে: ./lib/secret.js, তারপর আপনার client component-এর ফাইল, তারপর যে server পাতা সেটি ব্যবহার করে। অর্থাৎ ভুলটি যেখানে ঘটেছে (secret), কে টেনেছে (client ফাইল), আর পথ কোথা থেকে শুরু (page).

    দ্বিতীয় build সফল হবে, আর secret ফাইলে কোডে লেখা কোনো string থাকলে (যেমন fallback মান) সেটি কোনো chunk-এ পাবেন, যদি client component সত্যিই সেটি ব্যবহার করে। process.env.X-এর মান যাবে না (NEXT_PUBLIC_ না থাকলে), কিন্তু কোডের literal যাবে। শিক্ষা: পাহারাদার না থাকলে build আপনাকে সাবধান করবে না।

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

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

ক্রমNext.js App Router-এ একটি পাতার প্রথম অনুরোধের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সার্ভারে Server Component চলে, ডেটা আনে
  2. Server অংশের ফল আর client অংশের ঠিকানা দিয়ে RSC payload তৈরি হয়
  3. Payload আর Client Component মিলিয়ে সার্ভারে HTML তৈরি হয়
  4. ব্রাউজার HTML দেখায়, তখনো ক্লিক কাজ করে না
  5. JS নামার পর client দ্বীপগুলো hydrate হয়ে সচল হয়

Next.js App Router-এ একটি Client Component ("use client" আছে) সরাসরি URL খুললে কোথায় কোথায় চলে?

  • শুধু ব্রাউজারে
  • সার্ভারে HTML বানাতে, তারপর ব্রাউজারে hydration-এ
  • শুধু সার্ভারে
  • শুধু build-এর সময়

reviews.js-এর মাথায় "use client" যোগ করা হলো। ফাইলটি marked import করে, কিন্তু marked-এর ফাইলে কোনো directive নেই। marked কোথায় যাবে?

  • শুধু সার্ভারে, কারণ তার ফাইলে directive নেই
  • Build error হবে
  • Client bundle-এ, কারণ সীমানার ফাইল তাকে import করেছে
  • কোথাও না, tree shaking বাদ দেবে

একটি Server Component থেকে Client Component-কে নিচের কোন prop নিরাপদে দেওয়া যায়?

  • { listedAt: new Date() }
  • { format: (n) => '৳' + n }
  • একটি class Taka-এর object
  • { key: Symbol('x') }

একটি client Modal-এর ভেতরে server-এ ডেটা পড়া Cart দেখাতে চান। সঠিক উপায় কোনটি?

  • Modal-এর ফাইলে Cart import করা
  • Cart-এর মাথায় "use client" দেওয়া
  • Context দিয়ে Cart পাঠানো
  • Server পাতায় <Modal><Cart /></Modal> লিখে children হিসেবে দেওয়া

Database-এর ফাইলের মাথায় import 'server-only' রাখার ফল কী?

  • ফাইলের secret encrypt হয়ে ব্রাউজারে যায়
  • কোনো Client Component সেটি import করলে build error হয়
  • Server Component থেকেও import করা যায় না
  • সব environment variable স্বয়ংক্রিয়ভাবে public হয়

Next.js-এর RSC payload-এ একটি লাইন: ["$","$L16",null,{"images":["/a.jpg"]}]. এখানে $L16 কী বোঝায়?

  • একটি Server Component-এর কোড
  • ১৬ নম্বর ছবি
  • একটি Client Component-এর জায়গা, যার ঠিকানা ১৬ নম্বর লাইনে
  • ১৬ নম্বর লাইনে error

একটি npm package-এর carousel component ভেতরে useState ব্যবহার করে, কিন্তু তার ফাইলে "use client" নেই। Server Component থেকে ব্যবহার করতে সবচেয়ে ভালো উপায় কোনটি?

  • নিজের ফাইলে "use client" দিয়ে carousel re-export করা wrapper বানানো
  • node_modules-এ package-এর ফাইলে directive লিখে দেওয়া
  • পুরো পাতার মাথায় "use client" দেওয়া
  • Next.js config-এ hook চালু করা

শব্দকোষ

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

মনে রাখুন

  1. Server Component শুধু সার্ভারে চলে, async হতে পারে, database আর secret পড়ে; কোড ব্রাউজারে যায় না। Client Component সার্ভারে HTML বানায়, তারপর ব্রাউজারেও চলে।
  2. "use client" module graph-এ সীমানা; সেই ফাইলের সব import client bundle-এ যায়। সীমানা যত নিচে, bundle তত ছোট।
  3. সীমানা পার হয় শুধু serializable props: সাধারণ object, Date, Map, Set, JSX, Server Function. সাধারণ function আর class-এর object error দেয়। দরকারি ঘরটুকুই দিন।
  4. Client-এর ভেতরে server অংশ চাইলে children বা slot prop; import নয়। Context provider আর তৃতীয় পক্ষের hook-ওয়ালা component client wrapper-এ।
  5. RSC payload নেটওয়ার্ক ট্যাবে পড়া যায়; গোপন module-এ import 'server-only', আর build-এর পর নিজে মেপে দেখুন।

পরের পর্বে: Server Component await করতে পারে, এটুকু জানলেন। কিন্তু একই ডেটা কি প্রতি অনুরোধে আবার আনা হবে? পর্ব ৫৩-এ fetch, cache, revalidate আর Suspense দিয়ে streaming.

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

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