Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে

Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে

Z
জাহিদ হাসান তন্ময়
৭ নভেম্বর, ২০২৬
13 মিনিটের পড়া
🇬🇧 Read in English
|
0 views
অডিও ওভারভিউ ও পডকাস্ট
~১ মিনিট কুইক ওভারভিউ
0%

Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে
Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে

আমাদের আগের পর্বে আমরা যখন Agent-এর পুরো Conversation History Supabase-এ Save করে রাখা নিয়ে কাজ করেছিলাম, তখন src/app/page.tsx ফাইলের ভেতরে সার্ভার থেকে সরাসরি পুরনো চ্যাট লোড করে তা initialMessages হিসেবে আমাদের চ্যাট কম্পোনেন্টে পাস করেছিলাম। একটি সিঙ্গেল চ্যাট বক্সের জন্য সেই কাজটি বেশ সহজ ছিল। কিন্তু একটি পূর্ণাঙ্গ AI Agent অ্যাপ্লিকেশনে যখন সেশন হিস্ট্রি সাইডবার, এজেন্টের স্ট্যাটাস হেডার, ডকুমেন্টেশন প্রিভিউয়ার, লাইভ থিংকিং টাইমলাইন এবং Level 2 টুল কনফার্মেশন বাটন একসাথে যুক্ত হয়—তখন প্রায় প্রতিটি Next.js ডেভেলপারই একটি বড় আর্কিটেকচারাল প্রশ্নের মুখোমুখি হন: এই পুরো সিস্টেমের ঠিক কোন অংশটুকু Server Component (RSC) হবে, আর কোন অংশটুকু Client Component ('use client')-এ বসবে?

একটি প্রোডাকশন Next.js AI Agent আর্কিটেকচারে ডেটাবেস থেকে প্রাথমিক চ্যাট হিস্ট্রি লোড করা (loadSessionHistory), সাইডবারের সেশন লিস্ট দেখানো এবং সার্ভার-অনলি টুল কনফিগারেশন রিড করার দায়িত্ব থাকবে Server Component-এর ওপর; আর রিয়েল-টাইম টোকেন স্ট্রিমিং (useChat), AbortController (stop()), লাইভ থিংকিং টাইমার (100ms interval) এবং ইন্টারঅ্যাক্টিভ টুল অ্যাপ্রুভাল বাটনগুলো থাকবে একদম নিচের স্তরের (Leaf Node) Client Component-এ। এই দুই জগতকে React-এর Slot (children / ReactNode) প্যাটার্ন দিয়ে যুক্ত করলে আপনার গোপন API Key ও ডাটাবেস লজিক কখনোই ব্রাউজারে ফাঁস হয় না এবং ইউজার পেজ রিফ্রেশ করার সাথে সাথে কোনো লোডিং স্পিনার ছাড়াই সম্পূর্ণ চ্যাট হিস্ট্রি দেখতে পান।

আজকের এই ২৫তম পর্বের মাধ্যমে আমরা আমাদের ইমপ্লিমেন্টেশন মডিউলের সর্বশেষ ধাপে (Phase 4: Architectural Boundaries & Full Integration, পর্ব ২৫–২৬) প্রবেশ করছি। আজ আমরা আমাদের DevPulse এজেন্টের Server এবং Client কম্পোনেন্টগুলোর সীমানা নিখুঁতভাবে সাজিয়ে নেব, যাতে আগামী পর্বে (পর্ব ২৬) পুরো প্রজেক্টটি এক সুতোয় গেঁথে ফেলা যায়।

সাধারণ ওয়েব অ্যাপের চেয়ে AI Agent-এ Server বনাম Client বাউন্ডারি কেন দ্বিগুণ গুরুত্বপূর্ণ?

সাধারণ একটি পোর্টফোলিও বা ব্লগ সাইটে আপনি যদি ভুল করে পুরো পেজের উপরে 'use client' লিখেও ফেলেন, তবে বড়জোর জাভাস্ক্রিপ্ট বান্ডেলের সাইজ কয়েক কিলোবাইট বাড়ে। কিন্তু একটি মাল্টি-স্টেপ AI Agent প্রজেক্টে ভুল জায়গায় 'use client' বসালে সাথে সাথে ৩টি বড় টেকনিক্যাল বিপর্যয় ঘটে:

১. server-only বিল্ড ক্র্যাশ এবং সিক্রেট ফাঁস হওয়ার ঝুঁকি: আমাদের এজেন্টের টুল রেজিস্ট্রি এবং ডাটাবেস স্টোরে OPENAI_API_KEY, ANTHROPIC_API_KEY, এবং SUPABASE_SERVICE_ROLE_KEY ব্যবহৃত হয়। কোনো 'use client' ফাইল যদি প্রত্যক্ষ বা পরোক্ষভাবে (Transitive Import) এমন কোনো ফাইল ইমপোর্ট করে ফেলে যেখানে সার্ভার-অনলি মডিউল আছে, তবে Next.js সাথে সাথে বিল্ড ফেল করিয়ে দেবে। ২. ব্রাউজারে ডেটা লোডিং ওয়াটারফল (Client Fetch Waterfall): আপনি যদি সাইডবারের সেশন লিস্ট এবং চ্যাট হিস্ট্রি লোড করার কাজটি Server Component-এ না রেখে Client Component-এর useEffect-এর ভেতরে রাখেন, তবে ইউজার পেজে ঢোকার পর প্রথমে সাদা HTML দেখবে, তারপর জাভাস্ক্রিপ্ট ডাউনলোড হবে, তারপর /api/sessions কল হবে এবং সবশেষে /api/messages কল হবে—অর্থাৎ প্রতিবার পেজ লোডে ইউজারকে ২-৩টি লোডিং স্কেলিটন দেখতে হবে! ৩. সেকেন্ডে ৮০ টোকেন স্ট্রিমিংয়ের সময় পুরো পেজ Re-render হওয়া: যখন useChat হুক সেকেন্ডে ৭০-৮০টি করে শব্দ স্ট্রিম করতে থাকে, তখন ওই স্টেট থাকা কম্পোনেন্টটি প্রতি সেকেন্ডে অসংখ্যবার রি-রেন্ডার হয়। আপনার সাইডবার, হেডার এবং পুরো পেজ লেআউট যদি ওই একই Client Component-এর ভেতরে থাকে, তবে প্রতি টোকেনে পুরো পেজটিই অযথা রি-রেন্ডার হতে থাকবে।

এজেন্ট আর্কিটেকচারে কোন কম্পোনেন্ট কোথায় বসবে? (Responsibility Matrix)

কোড লেখার আগে চলুন এক নজরে দেখে নিই আমাদের DevPulse ওয়ার্কস্পেসের কোন অংশ কোথায় থাকবে:

  • মেইন ওয়ার্কস্পেস পেজ (src/app/page.tsx) -> Server Component: এটি URL থেকে ?session= প্যারামিটার রিড করবে এবং সার্ভারে বসেই Promise.all দিয়ে চ্যাট হিস্ট্রি ও সেশন লিস্ট একসাথে লোড করবে।
  • সেশন হিস্ট্রি সাইডবার (SessionSidebar.tsx) -> Server Component: সরাসরি Supabase-এর agent_sessions টেবিল থেকে পুরনো সেশনগুলোর তালিকা রেন্ডার করবে—এতে ব্রাউজারে শূন্য বাইট (0 KB) জাভাস্ক্রিপ্ট যাবে।
  • এজেন্ট কনফিগ ও টুল স্ট্যাটাস হেডার -> Server Component: আমাদের blueprint.ts এবং vercel-tools.ts থেকে অ্যাক্টিভ টুলের নাম ও Autonomy Level রিড করে স্ট্যাটিক ব্যাজ দেখাবে।
  • লাইভ স্ট্রিমিং চ্যাট বক্স (DevPulseStreamChat.tsx) -> Client Component ('use client'): শুধুমাত্র চ্যাট মেসেজ লিস্ট, ইনপুট বক্স, useSmartAutoScroll এবং Stop বাটন সামলাবে।
  • থিংকিং টাইমলাইন ও টুল কার্ড (AgentThinkingTimeline.tsx / TypeSafeToolRenderer.tsx) -> Client Component ('use client'): ১০০ মিলিসেকেন্ডের লাইভ টাইমার, অ্যাকর্ডিয়ন টগল এবং Level 2 টিকেট অ্যাপ্রুভাল বাটনের ক্লিক ইভেন্ট হ্যান্ডেল করবে।

Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে
Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে

নিচের ডায়াগ্রামে দেখুন কীভাবে Server Runtime থেকে ডেটা এবং কম্পোনেন্ট স্লটগুলো সিরিয়ালাইজেশন বাউন্ডারি পার হয়ে ব্রাউজারের Client Leaf কম্পোনেন্টে প্রবেশ করে:

📊 Architecture DiagramArchitecture Flow
Interactive diagram rendering...
flowchart TD
    subgraph Server [Next.js Server Runtime - Zero Browser JS]
        Page[Workspace Page RSC - page.tsx] -->|Parallel Promise.all| DB[(Supabase PostgreSQL)]
        Page --> SidebarRSC[SessionSidebar RSC - Server Rendered List]
        Page --> HeaderRSC[AgentStatusHeader RSC - Static Model & Tool Specs]
        API[POST /api/chat Route Handler] --> Tools[Server Tool Registry + LLM SDKs]
    end

    subgraph Boundary [Serialization Boundary - Plain JSON Props & ReactNodes]
        Props[initialMessages + sessionKey + sidebarSlot + headerSlot]
    end

    subgraph Client [Browser Runtime - Leaf Client Components]
        Shell[AgentWorkspaceShell - use client Drawer Toggle]
        ChatLeaf[DevPulseStreamChat - use client useChat Hook]
        TimelineLeaf[AgentThinkingTimeline - use client 100ms Timer]
        ToolLeaf[TypeSafeToolRenderer - use client Level 2 Approval]
    end

    Page --> Props
    SidebarRSC --> Props
    HeaderRSC --> Props
    Props --> Shell
    Shell --> ChatLeaf
    ChatLeaf --> TimelineLeaf
    ChatLeaf --> ToolLeaf
    ChatLeaf <-->|SSE Data Stream| API
System architecture specification and node flow: flowchart TD subgraph Server [Next.js Server Runtime - Zero Browser JS] Page[Workspace Page RSC - page.tsx] -->|Parallel Promise.all| DB[(Supabase PostgreSQL)] Page --> SidebarRSC[SessionSidebar RSC - Server Rendered List] Page --> HeaderRSC[AgentStatusHeader RSC - Static Model & Tool Specs] API[POST /api/chat Route Handler] --> Tools[Server Tool Registry + LLM SDKs] end subgraph Boundary [Serialization Boundary - Plain JSON Props & ReactNodes] Props[initialMessages + sessionKey + sidebarSlot + headerSlot] end subgraph Client [Browser Runtime - Leaf Client Components] Shell[AgentWorkspaceShell - use client Drawer Toggle] ChatLeaf[DevPulseStreamChat - use client useChat Hook] TimelineLeaf[AgentThinkingTimeline - use client 100ms Timer] ToolLeaf[TypeSafeToolRenderer - use client Level 2 Approval] end Page --> Props SidebarRSC --> Props HeaderRSC --> Props Props --> Shell Shell --> ChatLeaf ChatLeaf --> TimelineLeaf ChatLeaf --> ToolLeaf ChatLeaf <-->|SSE Data Stream| API

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): সবার উপরে থাকা page.tsx Server Component সার্ভার সাইডে প্যারালাল কুয়েরি (Promise.all) চালিয়ে Supabase থেকে ডেটা আনে এবং SessionSidebar ও AgentStatusHeader-কে পিওর Server Component হিসেবে রেন্ডার করে। এরপর সিরিয়ালাইজেশন বাউন্ডারি পার হয়ে সেই সার্ভার-রেন্ডারড অংশগুলো ReactNode স্লট হিসেবে এবং initialMessages সাধারণ JSON হিসেবে নিচের স্তরের Client Component (AgentWorkspaceShell ও DevPulseStreamChat)-এ যায়, যা শুধুমাত্র লাইভ স্ট্রিমিং এবং ইউজারের বাটন ক্লিকগুলো পরিচালনা করে।

ধাপ ১: Server থেকে Client বাউন্ডারি পারাপারের টাইপ কন্ট্রাক্ট (boundary-types.ts)

মনে রাখবেন, Server Component থেকে Client Component-এ যে প্রপসগুলো (Props) পাঠানো হয়, সেগুলোকে অবশ্যই React Flight Serialization নিয়ম মানতে হয়। অর্থাৎ আপনি কোনো ফাংশন, ডেটাবেস ইনস্ট্যান্স বা সরাসরি Zod স্কিমা অবজেক্ট প্রপস হিসেবে পাঠাতে পারবেন না।

চলুন src/lib/agent/boundary-types.ts ফাইলটি তৈরি করি, যেখানে শুধুমাত্র সিরিয়ালাইজযোগ্য সাধারণ ডেটা স্ট্রাকচারগুলো ডিফাইন করা থাকবে:

typescript
// src/lib/agent/boundary-types.ts
import { type Message } from 'ai';
import { type AutonomyLevel } from './blueprint';

export interface SerializableSessionItem {
  id: string;
  externalSessionKey: string;
  title: string;
  activeModel: 'gpt-4o' | 'claude-3-5-sonnet-latest';
  updatedAtIso: string;
}

export interface SerializableAgentBadgeInfo {
  autonomyLevel: AutonomyLevel;
  maxSteps: number;
  registeredToolNames: string[];
}

export interface AgentWorkspaceClientProps {
  activeSessionKey: string;
  initialMessages: Message[];
  agentBadge: SerializableAgentBadgeInfo;
}

এবার আমাদের ২৪তম পর্বের src/lib/agent/history-store.ts ফাইলের নিচে loadRecentSessions() নামের এই ছোট ফাংশনটি যুক্ত করে নিই, যা সাইডবারে দেখানোর জন্য সর্বশেষ ১৫টি সেশনের তালিকা নিয়ে আসবে:

typescript
// src/lib/agent/history-store.ts ফাইলে যুক্ত করুন
import { type SerializableSessionItem } from './boundary-types';

interface AgentSessionRow {
  id: string;
  external_session_key: string;
  title: string;
  active_model: 'gpt-4o' | 'claude-3-5-sonnet-latest';
  updated_at: string;
}

export async function loadRecentSessions(
  limit = 15
): Promise<SerializableSessionItem[]> {
  const supabase = getSupabaseAdmin();

  const { data: rows, error } = await supabase
    .from('agent_sessions')
    .select('id, external_session_key, title, active_model, updated_at')
    .order('updated_at', { ascending: false })
    .limit(limit);

  if (error || !rows) {
    return [];
  }

  return (rows as AgentSessionRow[]).map((row) => ({
    id: row.id,
    externalSessionKey: row.external_session_key,
    title: row.title,
    activeModel: row.active_model,
    updatedAtIso: new Date(row.updated_at).toISOString(),
  }));
}

ধাপ ২: সাইডবারের জন্য পিওর Server Component তৈরি (SessionSidebar.tsx)

আমাদের সেশন লিস্ট সাইডবারে শুধু আগের সেশনগুলোর লিংক এবং তারিখ দেখানো হবে—এখানে কোনো কমপ্লেক্স ব্রাউজার স্টেট বা হুকের দরকার নেই। তাই আমরা src/components/SessionSidebar.tsx-কে রাখব একটি পিওর Server Component হিসেবে (এখানে কোনো 'use client' থাকবে না):

tsx
// src/components/SessionSidebar.tsx
import Link from 'next/link';
import { type SerializableSessionItem } from '@/lib/agent/boundary-types';

interface SessionSidebarProps {
  sessions: SerializableSessionItem[];
  activeSessionKey: string;
}

export default function SessionSidebar({
  sessions,
  activeSessionKey,
}: SessionSidebarProps) {
  return (
    <aside className='w-64 shrink-0 bg-slate-950 border-r border-slate-800 p-4 flex flex-col justify-between'>
      <div className='space-y-4'>
        <div className='flex items-center justify-between'>
          <span className='text-xs font-bold uppercase tracking-wider text-slate-400'>
            Triage Sessions
          </span>
          <Link
            href={`/?session=devpulse-${Date.now()}`}
            className='px-2.5 py-1 rounded bg-cyan-600/20 border border-cyan-500/40 text-cyan-300 text-xs font-medium hover:bg-cyan-600/30'
          >
            + New
          </Link>
        </div>

        <nav className='space-y-1.5'>
          {sessions.length === 0 && (
            <p className='text-xs text-slate-500 py-4'>
              No saved sessions yet. Start a query to persist history.
            </p>
          )}

          {sessions.map((item) => {
            const isActive = item.externalSessionKey === activeSessionKey;
            return (
              <Link
                key={item.id}
                href={`/?session=${encodeURIComponent(item.externalSessionKey)}`}
                className={`block p-2.5 rounded-lg text-xs transition-colors ${
                  isActive
                    ? 'bg-cyan-950/60 border border-cyan-700/60 text-cyan-200'
                    : 'bg-slate-900/50 border border-slate-800/60 text-slate-300 hover:bg-slate-900'
                }`}
              >
                <div className='font-medium truncate'>{item.title}</div>
                <div className='text-[10px] text-slate-500 font-mono mt-1 flex justify-between'>
                  <span>{item.activeModel}</span>
                  <span>{item.updatedAtIso.slice(0, 10)}</span>
                </div>
              </Link>
            );
          })}
        </nav>
      </div>

      <div className='pt-4 border-t border-slate-900 text-[11px] text-slate-500 font-mono'>
        RSC Hydrated • Zero Client JS
      </div>
    </aside>
  );
}

ধাপ ৩: Slot Pattern দিয়ে Client Shell-এর ভেতরে Server Component বসানো

এখানে একটি চমৎকার আর্কিটেকচারাল কৌশল শিখে নিন। ধরুন মোবাইল স্ক্রিনে আপনি একটি মেনু বাটন দিয়ে SessionSidebar খোলা ও বন্ধ করার (isMobileSidebarOpen স্টেট) ব্যবস্থা করতে চান। তার মানে আপনার লেআউট র‍্যাপারটিতে useState লাগবে, অর্থাৎ সেটি হবে 'use client'।

কিন্তু আপনি যদি ওই 'use client' ফাইলের উপরে import SessionSidebar from './SessionSidebar' লিখে ফেলেন, তবে Next.js সাথে সাথে আপনার SessionSidebar-কেও Client Component বানিয়ে ফেলবে! কারণ 'use client' ফাইলের ভেতরে যা ইমপোর্ট করা হয়, তার সবই ক্লায়েন্ট বান্ডেলে ঢুকে যায়।

এর সমাধান হলো React Slot Pattern (sidebarSlot: ReactNode)। আমরা AgentWorkspaceShell.tsx-এ সরাসরি সাইডবার ইমপোর্ট না করে প্রপস হিসেবে একটি স্লট (sidebarSlot) নেব। ফলে মোবাইল ড্রয়ার বাটনটি ক্লায়েন্টে কাজ করবে, অথচ তার পেটের ভেতরে থাকা SessionSidebar শতভাগ Server Component-ই থেকে যাবে!

tsx
// src/components/AgentWorkspaceShell.tsx
'use client';

import { useState, type ReactNode } from 'react';

interface AgentWorkspaceShellProps {
  sidebarSlot: ReactNode;
  headerSlot: ReactNode;
  children: ReactNode;
}

export default function AgentWorkspaceShell({
  sidebarSlot,
  headerSlot,
  children,
}: AgentWorkspaceShellProps) {
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);

  return (
    <div className='min-h-screen bg-slate-950 text-slate-100 flex flex-col'>
      <header className='border-b border-slate-800 px-4 py-3 flex items-center justify-between bg-slate-900/60'>
        <div className='flex items-center gap-3'>
          <button
            type='button'
            onClick={() => setIsMobileSidebarOpen((prev) => !prev)}
            className='md:hidden px-2.5 py-1 rounded bg-slate-800 text-xs text-slate-200'
          >
            {isMobileSidebarOpen ? 'Close Menu' : 'Sessions ☰'}
          </button>
          {headerSlot}
        </div>
      </header>

      <div className='flex-1 flex overflow-hidden'>
        <div
          className={`${
            isMobileSidebarOpen ? 'block' : 'hidden'
          } md:block shrink-0`}
        >
          {sidebarSlot}
        </div>

        <main className='flex-1 overflow-y-auto p-4 md:p-6'>{children}</main>
      </div>
    </div>
  );
}

ধাপ ৪: মেইন Server Component পেজে সবকিছু একত্রিত করা (src/app/page.tsx)

এবার দেখুন আমাদের মূল পেজ src/app/page.tsx কীভাবে একজন দক্ষ কন্ডাক্টরের মতো সার্ভার থেকেই সব ডেটা এনে স্লটগুলোর ভেতরে বসিয়ে দিচ্ছে:

tsx
// src/app/page.tsx
import { defaultDevPulseConfig } from '@/lib/agent/blueprint';
import {
  loadSessionHistory,
  loadRecentSessions,
} from '@/lib/agent/history-store';
import { devPulseTools } from '@/lib/agent/vercel-tools';
import SessionSidebar from '@/components/SessionSidebar';
import AgentWorkspaceShell from '@/components/AgentWorkspaceShell';
import DevPulseStreamChat from '@/components/DevPulseStreamChat';

interface PageProps {
  searchParams: Promise<{ session?: string }>;
}

export default async function DevPulseWorkspacePage({ searchParams }: PageProps) {
  const params = await searchParams;
  const activeSessionKey = params.session ?? 'devpulse-prod-session-01';

  const [initialMessages, recentSessions] = await Promise.all([
    loadSessionHistory(activeSessionKey, 40),
    loadRecentSessions(15),
  ]);

  const registeredToolNames = Object.keys(devPulseTools);

  const headerSummary = (
    <div className='flex items-center gap-3 text-xs'>
      <span className='font-semibold text-cyan-400'>DevPulse Workspace</span>
      <span className='px-2 py-0.5 rounded bg-slate-800 text-slate-300 font-mono text-[11px]'>
        {defaultDevPulseConfig.autonomyLevel}
      </span>
      <span className='hidden sm:inline text-slate-400 font-mono text-[11px]'>
        Tools ({registeredToolNames.length}): {registeredToolNames.join(', ')}
      </span>
    </div>
  );

  return (
    <AgentWorkspaceShell
      headerSlot={headerSummary}
      sidebarSlot={
        <SessionSidebar
          sessions={recentSessions}
          activeSessionKey={activeSessionKey}
        />
      }
    >
      <DevPulseStreamChat
        sessionKey={activeSessionKey}
        initialMessages={initialMessages}
      />
    </AgentWorkspaceShell>
  );
}

লক্ষ্য করুন, devPulseTools ফাইলটিতে কিন্তু import 'server-only' দেওয়া আছে। যেহেতু page.tsx নিজে একটি Server Component, তাই সে অনায়াসে Object.keys(devPulseTools) কল করে টুলগুলোর নামের তালিকা বের করে স্ট্যাটিক HTML হিসেবে headerSlot-এ পাঠিয়ে দিচ্ছে—অথচ ব্রাউজারের ক্লায়েন্ট বান্ডেলে টুলের কোনো কোডই যাচ্ছে না!

Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে
Client Component আর Server Component — Agent Architecture-এ কোনটা কোথায় বসবে

আমার নিজের একটি অভিজ্ঞতা: মেইন পেজের শুরুতে 'use client' লিখে যখন পুরো বিল্ড এবং লোডিং পারফরম্যান্স নষ্ট করেছিলাম!

আমি যখন আমার আগের SPA React প্রজেক্টগুলো (যেমন Movie Explorer) তৈরি করেছিলাম, তখন সব ফাইলেই সরাসরি useState এবং useEffect লেখা যেত কারণ সেখানে সব কম্পোনেন্টই ছিল ক্লায়েন্ট কম্পোনেন্ট। সেই অভ্যাসবশত আমি যখন প্রথমবার Next.js App Router-এ আমাদের এজেন্ট ড্যাশবোর্ডের লেআউট বানাচ্ছিলাম, তখন মোবাইল সাইডবার খোলার একটি ছোট্ট const [open, setOpen] = useState(false) রাখার জন্য সোজা src/app/page.tsx-এর একদম প্রথম লাইনে 'use client' লিখে দিয়েছিলাম!

সাথে সাথে পরপর দুটি বড় বিপত্তি ঘটল।

প্রথমত, page.tsx ফাইলটিতে আমার এজেন্টের কনফিগ ও ডাটাবেস হেল্পার ইমপোর্ট করা ছিল, যার ভেতরে import 'server-only' দেওয়া ছিল। 'use client' লেখার সাথে সাথেই টার্মিনালে লাল রঙের বিল্ড এরর চলে এলো: 'You're importing a component that needs server-only. That only works in a Server Component.' দ্বিতীয়ত, সেটি সামাল দিতে গিয়ে আমি যখন ডাটাবেস কলগুলো সরিয়ে একটি ক্লায়েন্ট-সাইড useEffect-এর ভেতরে নিয়ে গেলাম, তখন দেখলাম প্রতিবার পেজ রিফ্রেশ দিলে আগে পুরো পেজ ফাঁকা আসে, তারপর ৬০০ মিলিসেকেন্ড ধরে লোডিং স্পিনার ঘোরে এবং সবশেষে সাইডবার ও মেসেজ রেন্ডার হয়!

এরপর যখন আমি page.tsx থেকে 'use client' সরিয়ে সেটিকে পুনরায় async Server Component বানালাম এবং মোবাইল ড্রয়ারের স্টেটটুকু আলাদা একটি ছোট AgentWorkspaceShell কম্পোনেন্টে সরিয়ে sidebarSlot ও children প্রপস ব্যবহার করলাম—তখন বিল্ড এরর যেমন নিমেষে গায়েব হয়ে গেল, তেমনি পেজ লোডের সময় কোনো স্পিনার ছাড়াই প্রথম ফ্রেমেই পুরো চ্যাট ও সাইডবার চলে আসতে শুরু করল।

Agent Architecture-এ Server ও Client Component মেশানোর সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে (পর্ব ২৬: সম্পূর্ণ মিনি-এজেন্ট প্রজেক্ট রিক্যাপ ও ফাইনাল ইন্টিগ্রেশন) যাওয়ার আগে নিশ্চিত হয়ে নিন আপনার কোডে যেন এই ৪টি ভুল না থাকে:

১. 'use client' ফাইলের ভেতরে সরাসরি Server Component ইমপোর্ট করা

মনে রাখবেন, কোনো ফাইলের উপরে 'use client' লেখা মানে ওই ফাইলটি যাদেরকে সরাসরি import করবে, তারা সবাই ক্লায়েন্ট বান্ডেলের অংশ হয়ে যাবে। তাই কখনোই ক্লায়েন্ট কম্পোনেন্টের ভেতরে সার্ভার কম্পোনেন্ট ইমপোর্ট করবেন না; সবসময় প্যারেন্ট Server Component (page.tsx) থেকে রেন্ডার করে children বা slot (ReactNode) প্রপস হিসেবে পাস করবেন।

২. প্রপস হিসেবে Zod Schema বা টুল ফাংশন পাস করা

Server Component থেকে Client Component-এ কখনোই devPulseTools অবজেক্ট বা searchDocsSchema-এর মতো Zod অবজেক্ট প্রপস হিসেবে পাঠাবেন না। এগুলোর ভেতরে মেথড থাকে যা React Flight সিরিয়ালাইজ করতে পারে না। শুধুমাত্র সাধারণ স্ট্রিং, নাম্বার, বুলিয়ান এবং Plain Object পাঠাবেন।

৩. প্রাথমিক চ্যাট হিস্ট্রি লোড করার জন্য Client useEffect ব্যবহার করা

পেজ ওপেন হওয়ার সময় আগের মেসেজগুলো লোড করার জন্য কখনোই ক্লায়েন্ট কম্পোনেন্টে useEffect + fetch() চালাবেন না। এতে অযথা নেটওয়ার্ক ওয়াটারফল তৈরি হয়। সবসময় Server Component-এ loadSessionHistory() কল করে useChat({ initialMessages })-এ পাস করবেন।

৪. সামান্য একটি বাটনের জন্য পুরো বড় কম্পোনেন্টকে 'use client' বানানো

ধরুন আপনার একটি বড় স্ট্যাটিক হেডার বা ডকুমেন্টেশন প্যানেলে শুধু একটি ছোট 'Copy Session ID' বাটন আছে যাতে onClick লাগবে। সেই একটি বাটনের জন্য পুরো ২০০ লাইনের কম্পোনেন্টটিকে 'use client' না বানিয়ে শুধুমাত্র ১০ লাইনের <CopyButton /> অংশটুকুকে আলাদা ক্লায়েন্ট কম্পোনেন্ট হিসেবে তৈরি করুন।

প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)

কোনো Client Component ছাড়া শুধুমাত্র Server Component দিয়েই কি পুরো AI Agent UI বানানো সম্ভব?

না। যদিও Server Component দিয়ে প্রাথমিক পেজ লোড এবং স্ট্যাটিক ডেটা চমৎকারভাবে রেন্ডার করা যায়, কিন্তু ইউজারের লাইভ টাইপিং স্টেট, সেকেন্ডে ৮০টি টোকেন রেন্ডার করা (useChat), মাঝপথে স্ট্রিম থামানোর AbortController (stop()), স্মার্ট অটো-স্ক্রলিং এবং ১০০ মিলিসেকেন্ডের লাইভ থিংকিং টাইমার চালানোর জন্য ব্রাউজার-সাইড ইভেন্ট লুপ বা Client Component ('use client') অপরিহার্য। তাই প্রোডাকশনে সবসময় দুটির সমন্বয় (Hybrid Architecture) ব্যবহার করা হয়।

page.tsx-এর ভেতরে আলাদা আলাদা await না লিখে আমরা কেন Promise.all ব্যবহার করলাম?

আপনি যদি প্রথমে await loadSessionHistory() এবং তার নিচের লাইনে await loadRecentSessions() লেখেন, তবে দ্বিতীয় কুয়েরিটি প্রথম কুয়েরি শেষ হওয়া পর্যন্ত অযথা বসে থাকবে (Sequential Waterfall)। কিন্তু দুটি কুয়েরিই যেহেতু একে অপরের ওপর নির্ভরশীল নয়, তাই Promise.all([...]) ব্যবহার করলে Supabase-এ দুটি কুয়েরি একই সময়ে সমান্তরালে (Parallel) রান হয় এবং সার্ভার রেসপন্স টাইম অর্ধেকে নেমে আসে।

Client Component-এ নতুন চ্যাট শেষ হওয়ার পর Server Component-এর SessionSidebar কীভাবে অটোমেটিক আপডেট হবে?

যখন কোনো ইউজার নতুন সেশনে প্রথম প্রশ্ন করেন এবং ব্যাকএন্ডের onFinish হুক সেটি Supabase-এ সেভ করে, তখন আপনি ফ্রন্টএন্ডের useChat-এর onFinish কলব্যাকের ভেতরে next/navigation-এর router.refresh() কল করতে পারেন। এটি ব্রাউজারের চ্যাট স্টেট বা স্ক্রল পজিশন নষ্ট না করেই ব্যাকগ্রাউন্ডে Server Component (SessionSidebar) থেকে নতুন সেশন লিস্টটি রি-ফেচ করে আপডেট করে দেয়।

🟢 ফ্রিল্যান্স ও কনট্রাক্ট প্রজেক্টের জন্য উন্মুক্ত

কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?

আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!

ফুল-স্ট্যাক এমভিপি (MVP) ডেভেলপমেন্ট
অটোনোমাস এআই এজেন্ট ও এলএলএম ইন্টিগ্রেশন
পারফেক্ট এসইও ও সর্বোচ্চ পারফরম্যান্স
ক্লিন কোড ও মডার্ন আর্কিটেকচার

আর্টিকেলটি কি আপনার ভালো লেগেছে?

তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!

0 views
Z

জাহিদ হাসান তন্ময়

সফটওয়্যার ডেভেলপার

MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।

Related Articles

Stay Updated

Subscribe to get insights on full-stack architecture, AI agent engineering, and web development.