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

Vue আর Nuxt — reactivity আর React-এর সাথে তুলনা

Vue and Nuxt — Reactivity and a Comparison with React

Taught in Bangla, this lesson introduces Vue's single-file components next to JSX, the Composition API with ref, reactive, computed and watch, reactivity through Proxy and dependency tracking, a real render count comparing React re-renders with Vue's targeted updates, props, emit, v-model, slots and directives, Pinia and Vue Router, Nuxt file routing, useFetch and server/api, and the same Bazarghar pages measured in both without declaring a winner.

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

  • Vue-র ইতিহাস, Vue 3-এর অবস্থান, আর template বনাম JSX-এর আসল তফাত
  • <script setup>-এ ref, reactive, computed, watch আর lifecycle hook
  • Proxy-ভিত্তিক reactivity নিজে ছোট করে বানিয়ে দেখা, আর React-এর re-render-এর সাথে সংখ্যায় মেলানো
  • Props, emit, v-model, slot, v-if, v-for, Pinia আর Vue Router-কে React-এর pattern-এর পাশে রাখা
  • Nuxt-এর file routing, useFetch, server/api, rendering mode, auto-import, আর কখন Vue বা Nuxt বেছে নেবেন

১ · Vue কোথা থেকে এলো, আর এখন কোথায়

এতক্ষণ পুরো কোর্সে আপনি React আর Next.js দিয়ে বাজারঘর বানিয়েছেন। এই মডিউলে একই দোকানের ছোট একটি অংশ অন্য framework দিয়ে আবার বানানো হবে। উদ্দেশ্য একটাই: নতুন framework দেখলে আপনি যেন বুঝতে পারেন, কোনটি আসলে নতুন ধারণা আর কোনটি শুধু অন্য নামে পুরোনো জিনিস।

একটি তুলনা দিয়ে শুরু করা যাক। দুটি রেস্তোরাঁ ভাবুন। প্রথমটিতে কোনো টেবিলের অর্ডার বদলালে ওয়েটার পুরো টেবিলের সব খাবার আবার রান্নাঘরে মিলিয়ে দেখেন, তারপর শুধু যা আলাদা তা বদলান। দ্বিতীয়টিতে প্রতিটি পদের পাশে একটি চিরকুট আছে, যাতে লেখা কোন অতিথি ওই পদ চেয়েছেন। ডালের দাম বদলালে শুধু ডাল নেওয়া অতিথিদের কাছে খবর যায়। প্রথমটি মোটামুটি React-এর ধরন, দ্বিতীয়টি Vue-র। এই পর্বের বাকিটা এই চিরকুটের ব্যবস্থা নিয়ে।

Vue-র সরকারি FAQ অনুযায়ী Evan You ২০১৪ সালে Vue একটি ব্যক্তিগত side project হিসেবে শুরু করেন। তিনি তখন AngularJS নিয়ে কাজ করতেন বলে জানা যায়। প্রচলিত গল্প হলো, তিনি AngularJS-এর data binding-এর সুবিধাটুকু রেখে অনেক হালকা কিছু চেয়েছিলেন। Vue 2 আসে ২০১৬ সালে, আর Vue 3 ২০২০ সালে। Vue 3-এ reactivity নতুন করে লেখা হয় JavaScript-এর Proxy দিয়ে, আর আসে Composition API. Vue 2-এর জীবন শেষ হয়েছে ৩১ ডিসেম্বর ২০২৩-এ। Vue কোনো বড় কোম্পানির নয়, একটি স্বাধীন community project, যা মূলত sponsorship-এ চলে।

লেখার সময়ে (নভেম্বর ২০২৬) npm-এ Vue-র stable version 3.5.44. Version 3.6 এখনো release candidate, অর্থাৎ পরীক্ষার পর্যায়ে, তাই এই পর্বের সব run 3.5.44-এ। পরিবারের বাকি সদস্যদের version-ও লিখে রাখুন, কারণ পর্ব জুড়ে এগুলোই চলেছে:

Packageকাজলেখার সময়ের versionReact-এ যার মতো
vueComponent, reactivity, template compiler3.5.44react + react-dom
vue-routerURL অনুযায়ী পাতা5.4.0React Router
piniaGlobal store4.1.0Zustand, Redux Toolkit
nuxtMeta-framework: SSR, file routing, server4.6.3Next.js
viteDev server আর build8.3.5একই Vite

একটি মজার যোগসূত্র আছে। পর্ব ৫-এ যে Vite দিয়ে React project বানিয়েছিলেন, সেটিও Evan You-এরই শুরু করা, প্রথমে Vue-র জন্য। তাই Vue-তে আসলে কাজের পরিবেশ প্রায় একই থাকে, বদলায় শুধু component লেখার ধরন।

$ npm create vite@9.3.3 vue-shop -- --template vue-ts --no-interactive
$ cd vue-shop && npm install
$ npm install vue-router@5.4.0 pinia@4.1.0

Vue দল নিজেও npm create vue@latest নামে একটি scaffolding tool দেয়, যা router, Pinia, test আর lint আগে থেকে যুক্ত করার প্রশ্ন করে। এখানে React-এর সাথে ন্যায্য তুলনার জন্য দুই দিকেই একই create-vite ব্যবহার করা হয়েছে।

২ · Single-file component আর template — JSX-এর পাশে

React-এ একটি component হলো একটি JavaScript function, যা JSX ফেরত দেয়। Vue-তে একটি component সাধারণত একটি .vue ফাইল, যার তিনটি অংশ: <script>-এ logic, <template>-এ markup, আর ঐচ্ছিক <style>. একে বলে single-file component বা SFC. দর্জির দোকানের কথা ভাবুন। মাপের খাতা, কাপড় আর নকশা এক থলেতে রাখা থাকে, তাই একটি অর্ডারের সবকিছু এক জায়গায় পাওয়া যায়।

বাজারঘরের পণ্যের কার্ড দুইভাবে, পাশাপাশি। দুটোই সত্যিকারের project থেকে, দুটোই build হয়েছে:

<script setup lang="ts">
import type { Product } from '../products';

const props = defineProps<{ product: Product }>();
const emit = defineEmits<{ add: [product: Product] }>();
</script>

<template>
  <li class="card">
    <RouterLink :to="`/products/${props.product.id}`">{{ props.product.name }}</RouterLink>
    <span>৳{{ props.product.price }}</span>
    <slot name="badge" />
    <button :disabled="props.product.stock === 0" @click="emit('add', props.product)">যোগ</button>
  </li>
</template>
import type { ReactNode } from 'react';
import { Link } from 'react-router';
import type { Product } from '../products';

type Props = { product: Product; onAdd: (p: Product) => void; badge?: ReactNode };

export default function ProductCard({ product, onAdd, badge }: Props) {
  return (
    <li className="card">
      <Link to={`/products/${product.id}`}>{product.name}</Link>
      <span>৳{product.price}</span>
      {badge}
      <button disabled={product.stock === 0} onClick={() => onAdd(product)}>যোগ</button>
    </li>
  );
}

পড়ার নিয়মগুলো এক জায়গায়, যাতে Vue কোড দেখলে আটকে না যান:

কাজVue templateReact JSX
লেখা বসানো{{ name }}{name}
Attribute-এ মান:to="url" (v-bind-এর ছোট রূপ)to={url}
Event@click="fn" (v-on)onClick={fn}
CSS classclassclassName
শর্তv-if, v-else? :, &&
তালিকাv-for="p in list" :key="p.id"list.map(p => …key={p.id})
দুই দিকের inputv-model="search"value + onChange

v-if, v-for, v-model-এর মতো v- দিয়ে শুরু হওয়া attribute-কে বলে directive. Template দেখতে HTML-এর মতো, কিন্তু browser এটি সরাসরি পড়ে না। Build-এর সময় Vue-র compiler এটিকে একটি render function-এ বদলে দেয়, যা আসলে JSX-এর মতোই একটি JavaScript function. তাহলে তফাত কোথায়? Template-এর গঠন বাঁধা, তাই compiler আগেই জানে কোন অংশ কখনো বদলাবে না আর কোন অংশ বদলাতে পারে। JSX পুরো JavaScript, তাই সেখানে এই অনুমান কঠিন।

মূল ধারণা

Template বনাম JSX রুচির প্রশ্ন যতটা, তার চেয়ে বেশি একটি trade-off. Template-এ স্বাধীনতা কম, কিন্তু compiler বেশি জানে। JSX-এ স্বাধীনতা বেশি, কিন্তু অপ্টিমাইজেশনের দায় বেশিটা runtime বা আপনার। Vue চাইলে JSX-ও চালাতে পারে, কিন্তু বেশিরভাগ Vue কোড template-এই লেখা হয়।

৩ · Composition API — ref, reactive, computed, watch আর lifecycle

Vue 3 দুই ধরনের লেখা সমর্থন করে। পুরোনো Options API-তে data, methods, computed আলাদা আলাদা খোপে লেখা হয়। নতুন Composition API-তে সাধারণ function দিয়ে logic সাজানো হয়, যা React-এর hook-এর কাছাকাছি লাগে। নতুন project-এ সাধারণত <script setup> দিয়ে Composition API লেখা হয়। এর ভেতরের সব variable আর import সরাসরি template-এ ব্যবহার করা যায়।

বাজারঘরের পণ্যের তালিকার আসল ফাইল:

<script setup lang="ts">
import { ref, computed } from 'vue';
import { products } from '../products';
import { useCart } from '../stores/cart';
import ProductCard from '../components/ProductCard.vue';

const cart = useCart();
const search = ref('');
const shown = computed(() =>
  products.filter((p) => p.name.toLowerCase().includes(search.value.toLowerCase())),
);
</script>

<template>
  <h1>পণ্যের তালিকা</h1>
  <input v-model="search" placeholder="খুঁজুন" />
  <p v-if="shown.length === 0">কিছু পাওয়া যায়নি</p>
  <ul v-else>
    <ProductCard v-for="p in shown" :key="p.id" :product="p" @add="cart.add">
      <template #badge>
        <small v-if="p.stock === 0">স্টক নেই</small>
      </template>
    </ProductCard>
  </ul>
</template>

প্রতিটি নতুন জিনিস এক এক করে:

  • ref(''): একটি reactive বাক্স। Script-এ মান পড়তে বা লিখতে search.value লাগে। Template-এ Vue নিজে .value খুলে দেয়, তাই সেখানে শুধু search. React-এর useState-এর মতো, তবে কোনো setter নেই, সরাসরি search.value = 'dal'.
  • reactive({...}): পুরো একটি object-কে reactive করে। .value লাগে না, তবে destructure করলে reactivity হারায়। এজন্য Vue-র docs সাধারণ ক্ষেত্রে ref-কেই প্রথম পছন্দ বলে।
  • computed(() => …): অন্য মান থেকে হিসাব করা মান। search না বদলালে Vue আগের ফল ফেরত দেয়। React-এ এর কাছাকাছি হলো useMemo, তবে সেখানে dependency array নিজে লিখতে হয়। Vue নিজে খুঁজে নেয়।
  • setup একবারই চলে: এটিই সবচেয়ে বড় মানসিক পরিবর্তন। React-এ component function প্রতিটি render-এ আবার চলে। Vue-তে <script setup> component তৈরির সময় একবার চলে। পরে শুধু template থেকে বানানো render function আবার চলে।

Side effect-এর জন্য watch, আর জীবনচক্রের জন্য কয়েকটি hook. ধরুন খোঁজার শব্দ URL-এ রাখতে চান, আর পাতা খোলার সময় একটি log চান:

import { ref, watch, onMounted, onUnmounted } from 'vue';

const search = ref('');

// search বদলালে চলে; React-এর useEffect(..., [search])-এর কাছাকাছি
watch(search, (now, before) => {
  console.log(`search: "${before}" → "${now}"`);
});

onMounted(() => console.log('DOM-এ বসেছে'));      // useEffect(..., [])-এর মতো
onUnmounted(() => console.log('সরানো হলো'));       // cleanup-এর মতো
ReactVue (Composition API)মূল পার্থক্য
useStateref, reactiveVue-তে মান সরাসরি বদলানো যায়, setter নেই
useMemocomputedVue dependency নিজে খোঁজে
useEffect (dependency সহ)watch, watchEffectwatchEffect যা পড়ে, তাতেই নির্ভর করে
useEffect(…, [])onMounted, onUnmountedআলাদা নামের hook
useRef (DOM)template ref, useTemplateRefএকই কাজ
Custom hookComposable (useCart)Hook-এর ক্রমের নিয়ম নেই, কারণ setup একবার চলে

Hook-এর নিয়ম Vue-তে কেন নেই? পর্ব ১৪-এ দেখেছেন, React-এ hook if-এর ভেতরে ডাকা যায় না, কারণ React প্রতিটি render-এ hook-এর ক্রম দিয়ে state চেনে। Vue-র setup একবারই চলে, আর ref একটি সাধারণ object. তাই composable শর্তের ভেতরেও ডাকা যায়। তবে একটি নিয়ম থাকে: onMounted-এর মতো lifecycle hook setup চলার সময়েই, synchronous ভাবে ডাকতে হয়।

৪ · Reactivity-র ভেতরে — Proxy আর dependency tracking

Vue কীভাবে জানে যে shown নির্ভর করে search-এর ওপর? আপনি তো কোনো dependency array লেখেননি। উত্তর হলো JavaScript-এর Proxy. Proxy একটি object-এর চারপাশে পাহারাদারের মতো বসে। কেউ property পড়লে বা লিখলে পাহারাদার আগে খবর পায়। JavaScript কোর্সের Proxy আর Reflect পর্বে এর খুঁটিনাটি আছে, এখানে শুধু ব্যবহারটুকু।

পুরো ব্যবস্থায় তিনটি কাজ:

  1. Effect চালানো: একটি function চালানোর আগে Vue তাকে “এখন চলছে” বলে চিহ্নিত করে। Vue-তে প্রতিটি component-এর render এমনই একটি reactive effect.
  2. Track: চলার সময় সে যা পড়ে, Proxy-র get তা ধরে। লিখে রাখা হয়: “এই effect এই object-এর এই key পড়েছে।” একে বলে dependency tracking.
  3. Trigger: পরে কেউ সেই key লিখলে Proxy-র set তালিকা খুলে শুধু সেই effect-গুলো আবার চালায়।

এটিই সেই রেস্তোরাঁর চিরকুট। এই ধারণা নতুন নয়। পর্ব ৪-এর observer pattern-এর একটি স্বয়ংক্রিয় রূপ, আর আজকাল একে প্রায়ই signal বলা হয়। নিচের ব্লকটি শেখানোর মডেল, আসল Vue নয়। আসল Vue এখানে লোড করা যায় না, তাই একই ধারণা প্রায় ২৫ লাইনে লেখা হয়েছে। চালিয়ে দেখুন কোন effect আবার চলে।

// শেখানোর মডেল: Vue-র reactivity-র মূল ধারণা, আসল Vue নয়
let active = null;                        // এখন কোন effect চলছে
const deps = new WeakMap();               // object → key → effect-এর Set
function track(obj, key) {
  if (!active) return;
  if (!deps.has(obj)) deps.set(obj, new Map());
  const byKey = deps.get(obj);
  if (!byKey.has(key)) byKey.set(key, new Set());
  byKey.get(key).add(active);             // "এই effect এই key পড়েছে"
}
const trigger = (obj, key) => deps.get(obj)?.get(key)?.forEach((run) => run());
const reactive = (obj) => new Proxy(obj, {
  get(t, k) { track(t, k); return t[k]; },
  set(t, k, v) { if (t[k] !== v) { t[k] = v; trigger(t, k); } return true; },
});
const ref = (value) => reactive({ value });
function effect(fn) {
  const run = () => { const prev = active; active = run; fn(); active = prev; };
  run();
}
function computed(getter) {               // সরল রূপ: আসল computed অলস (lazy)
  const r = ref(undefined);
  effect(() => { r.value = getter(); });
  return r;
}
const cart = reactive({ count: 2, shopper: 'Rafi' });
const price = ref(420);
const total = computed(() => cart.count * price.value);
effect(() => console.log('Badge ->', cart.count));
effect(() => console.log('Name  ->', cart.shopper));
effect(() => console.log('Total ->', total.value));
console.log('--- count = 3');   cart.count = 3;
console.log('--- price = 420'); price.value = 420;
console.log('--- shopper = Mina'); cart.shopper = 'Mina';

আউটপুট:

Badge -> 2
Name  -> Rafi
Total -> 840
--- count = 3
Total -> 1260
Badge -> 3
--- price = 420
--- shopper = Mina
Name  -> Mina

প্রথম তিন লাইন শুরুর run, যখন প্রতিটি effect একবার চলে আর নিজের পড়া key লিখে রাখে। count 3 হলে চলল শুধু total-এর হিসাব (৩ × ৪২০ = ১২৬০), তার পাঠক Total, আর Badge. Name চলেনি, কারণ সে count পড়েনি। price-এ আবার ৪২০ লেখায় কিছুই চলেনি, কারণ set আগে মান মিলিয়ে দেখে। আসল Vue আরও অনেক কিছু করে: পুরোনো dependency মুছে ফেলা, effect-গুলো একসাথে জমিয়ে পরের microtask-এ চালানো, আর computed-কে অলস রাখা, যাতে কেউ না পড়লে হিসাবই না হয়।

একই ঘটনা এবার ধাপে ধাপে, চিরকুটের খাতাটি খুলে:

1// deps: count → [computed, Badge]2//       shopper → [Name]3cart.count = 3;4set: 2 !== 3, তাই লেখা হয়5trigger(cart, 'count')6computed: total = 3 * 4207  → trigger(total, 'value')8Badge effect আবার চলে9Name: তালিকায় নেই
মান
cart.count
total.value
Badge দেখায়
Name দেখায়
চলা effect
  1. শুরু। প্রথম run-এ খাতা তৈরি হয়ে আছে। count পড়েছে computed আর Badge, shopper পড়েছে শুধু Name. Badge দেখাচ্ছে 2, মোট 840.
  2. লেখা। cart.count = 3 চলল। Proxy-র set দেখল আগের মান 2, নতুন 3, আলাদা। তাই মান লেখা হলো। এখনো কোনো effect চলেনি।
  3. Trigger. set খাতায় count-এর লাইন খুঁজল। পেল দুটি effect: computed আর Badge. Name-এর নাম এই লাইনে নেই।
  4. Computed আবার হিসাব করে। ৩ × ৪২০ = ১২৬০. total.value বদলাল, তাই এবার total-এর পাঠক Total effect চলল আর log করল 1260. এক বদল থেকে আরেক বদল, শিকলের মতো।
  5. Badge চলে। তালিকার দ্বিতীয় effect, Badge, এবার 3 দেখায়। মোট তিনটি effect চলল: computed, Total আর Badge.
  6. Name চুপ। Name আগের মতোই Rafi দেখাচ্ছে, তাকে কেউ ডাকেইনি। React-এ এই পরীক্ষাটি নেই। সেখানে দেখা হয় কোন component-এর state বদলেছে, আর সেই component ও তার নিচের সবাই আবার চলে।

একই ক্লিক, দুই রকম খবর

React re-runs a component subtree; Vue re-runs only the effects that read the changed value

React-এর subtree re-render বনাম Vue-র dependency-ভিত্তিক update React: count বদলাল Parent Badge Name Static Hits memo বাদ কমলা = আবার render Vue: count বদলাল ref count ref hits Parent Badge Name Hits Static বেগুনি তীর = কে কী পড়েছে শুধু পাঠক আবার চলে
ছবি ৬৬.১ — ওপরে React-এর ধরন: বাবার state বদলালে নিচের সবাই আবার চলে, যদি না memo দিয়ে আলাদা করা হয়। নিচে Vue-র ধরন: count-এর পাঠক Parent আর Badge-ই শুধু চলে। hits বদলালে চলবে শুধু Hits.

৫ · React-এর re-render বনাম Vue-র নির্দিষ্ট update — সত্যিকারের গোনা

তত্ত্ব শুনলে মনে হয় Vue “দ্রুত”। কিন্তু মতামত নয়, মাপ দেখা যাক। দুটি Vite project-এ হুবহু এক ছোট পাতা বানানো হলো। একটি Parent, তার চারটি সন্তান:

  • Badge: prop হিসেবে count পায় আর দেখায়।
  • Name: prop হিসেবে shopper পায়, যা কখনো বদলায় না।
  • Static: কিছুই পায় না, শুধু দোকানের ঠিকানা দেখায়।
  • Hits: একটি আলাদা store থেকে hits নিজে পড়ে। Vue-তে একটি module-এর ref, React-এ একটি Zustand store.

প্রতিটি component নিজের render গুনে console-এ লেখে। Vue-তে setup একবারই চলে, তাই গোনা হয়েছে onMounted আর onUpdated দিয়ে। React-এ গোনা হয়েছে function-এর ভেতরে। Vue-র গোনার অংশটুকু:

import { onMounted, onUpdated, ref } from 'vue';
const n: Record<string, number> = {};
export const hits = ref(0);
export function countRenders(name: string) {
  const log = () => console.log(`[render] ${name} ${(n[name] = (n[name] ?? 0) + 1)}`);
  onMounted(log);
  onUpdated(log);
}

সত্যিকারের run (Vue 3.5.44 আর React 19.3.1, দুটোই Vite 8.3.5-এর production build, vite preview, Edge-এ Playwright, StrictMode ছাড়া)। পাতা খোলা হলো, “যোগ” তিনবার আর “দেখা” দুবার চাপা হলো। Console থেকে নেওয়া:

== VUE after load: [render] Badge 1, [render] Name 1, [render] Static 1, [render] Hits 1, [render] Parent 1
== VUE after 3 x add: [render] Badge 2, [render] Parent 2, [render] Badge 3, [render] Parent 3, [render] Badge 4, [render] Parent 4
== VUE after 2 x hit: [render] Hits 2, [render] Hits 3
== REACT after load: [render] Parent 1, [render] Badge 1, [render] Name 1, [render] Static 1, [render] MemoName 1, [render] MemoStatic 1, [render] Hits 1
== REACT after 3 x add: [render] Parent 2, [render] Badge 2, [render] Name 2, [render] Static 2, [render] Hits 2, … (একই ধারা আরও দুবার) …, [render] Hits 4
== REACT after 2 x hit: [render] Hits 5, [render] Hits 6
ComponentVueReactReact, memo দিয়ে
Parent (count রাখে)444
Badge (count দেখায়)44—
Name (prop বদলায় না)141 (MemoName)
Static (prop নেই)141 (MemoStatic)
Hits (store পড়ে)36—

সংখ্যাগুলো সাবধানে পড়ুন। এক: Vue-তেও Parent ৪ বার চলেছে, কারণ তার template count পড়ে Badge-কে দেয়। Vue “কখনো component আবার চালায় না” এমন নয়। সে চালায় শুধু যে component মানটি পড়েছে। দুই: Parent চলার পর Vue প্রতিটি সন্তানের props আগের সাথে মেলায়। Name আর Static-এর props বদলায়নি, তাই তাদের ছেড়ে দিয়েছে। React-এ এই কাজটি করে memo, যা আপনাকে নিজে লিখতে হয়। তিন: React-এ Hits ৬, কারণ সে Parent-এর সন্তান। Parent চললে সেও চলে, store না বদলালেও।

নিয়মটি এবার একটি ছোট শেখানোর মডেল-এ। এটি আসল React বা Vue নয়, শুধু দুই নিয়মে গোনে। React-এর নিয়ম: যে component-এর state বদলেছে সে আর তার নিচের সবাই চলে, memo থাকলে props না বদলানো সন্তান বাদ যায়। Vue-র নিয়ম: যে মান পড়েছে সে চলে, আর সন্তান চলে শুধু তার prop বদলালে।

// শেখানোর মডেল: দুই ধরনের update গোনা, আসল React বা Vue নয়
const tree = {
  Parent: { reads: ['count', 'shopper'], kids: ['Badge', 'Name', 'Static', 'Hits'] },
  Badge: { props: ['count'], kids: [] },
  Name: { props: ['shopper'], kids: [] },
  Static: { props: [], kids: [] },
  Hits: { reads: ['hits'], kids: [] },
};
function run(style, changes, memo = false) {
  const n = {};
  for (const c in tree) n[c] = 1;                 // প্রথম render সবার একবার
  for (const key of changes) {
    const queue = Object.keys(tree).filter((c) => tree[c].reads?.includes(key));
    while (queue.length) {
      const c = queue.shift();
      n[c]++;
      for (const kid of tree[c].kids) {
        const propChanged = tree[kid].props?.includes(key) ?? false;
        if (style === 'react' && (!memo || propChanged)) queue.push(kid);
        if (style === 'vue' && propChanged) queue.push(kid);
      }
    }
  }
  return Object.entries(n).map(([c, k]) => `${c} ${k}`).join(', ');
}
const clicks = ['count', 'count', 'count', 'hits', 'hits'];
console.log('React       :', run('react', clicks));
console.log('React + memo:', run('react', clicks, true));
console.log('Vue         :', run('vue', clicks));

আউটপুট:

React       : Parent 4, Badge 4, Name 4, Static 4, Hits 6
React + memo: Parent 4, Badge 4, Name 1, Static 1, Hits 3
Vue         : Parent 4, Badge 4, Name 1, Static 1, Hits 3

মডেলের প্রথম আর তৃতীয় লাইন সত্যিকারের run-এর সাথে হুবহু মিলেছে। দ্বিতীয় লাইনে মডেল সব সন্তানকে memo দিয়েছে, তাই Hits-ও ৩. সত্যিকারের run-এ Hits-এ memo ছিল না বলে সেটি ৬ হয়েছিল। মূল শিক্ষা: Vue যা নিজে থেকে করে, React-এ তা memo দিয়ে চাইতে হয়। পর্ব ২৩-এর React Compiler এই memo নিজে যোগ করে দিতে পারে, তাই এই ফারাক React-এর দিক থেকেও কমছে।

“Vue-তে virtual DOM নেই” ভুল ধারণা। Vue 3.5-এও প্রতিটি component-এর ভেতরে virtual DOM আছে। Reactivity ঠিক করে কোন component আবার চলবে। তারপর সেই component-এর ভেতরে নতুন আর পুরোনো virtual DOM মেলানো হয়, পর্ব ১৫-এর মতোই। পার্থক্য হলো, compiler template দেখে আগে থেকে চিহ্ন দেয় কোন node কখনো বদলাবে না, তাই মেলানোর কাজ অনেক কম।

দামটাও জানা দরকার। প্রতিটি reactive মানের জন্য পাঠকের তালিকা রাখতে মেমরি লাগে। আর একটি নতুন ফাঁদ আসে: reactive object destructure করলে সংযোগ ছিঁড়ে যায়। const { count } = reactive(state) লিখলে count একটি সাধারণ সংখ্যা, Proxy আর নেই। React-এ এই ফাঁদ নেই, কারণ সেখানে প্রতিটি render নতুন করে মান পড়ে। কোনো মডেল বিনা খরচে আসে না।

৬ · Props, emit, v-model, slot আর directive — React-এর pattern-এর পাশে

পর্ব ৯ থেকে পর্ব ১২ পর্যন্ত React-এ যা শিখেছেন, Vue-তে তার প্রায় সবই আছে, শুধু নাম আর লেখার ধরন আলাদা। এক টেবিলে:

কাজReactVue
বাবা থেকে সন্তানে মানprops (function-এর argument)defineProps
সন্তান থেকে বাবাকে খবরcallback prop (onAdd)emit (defineEmits, @add)
Input আর state জোড়াcontrolled input: value + onChangev-model
ভেতরে markup বসানোchildren, JSX propslot, নামধারী slot (#badge)
শর্তে দেখানো? :, && (পর্ব ১১)v-if, v-else-if, v-else, v-show
তালিকাmap + keyv-for + :key

Emit: সন্তান বাবাকে খবর দেয়

React-এ সন্তান বাবার দেওয়া একটি function ডাকে। Vue-তে সন্তান একটি নাম দেওয়া event “ছোড়ে”, আর বাবা @add দিয়ে সেটি ধরে। অফিসের পিয়ন ভাবুন। তিনি সরাসরি ম্যানেজারের খাতায় লেখেন না, শুধু একটি চিরকুট দিয়ে যান: “৩ নম্বর পণ্য যোগ করতে বলেছে।” কী করা হবে, সেটি ম্যানেজারের সিদ্ধান্ত। নিচে একটি ক্লিক বাজারঘরের ঝুড়ি পর্যন্ত কীভাবে পৌঁছায়, দেখুন। শুরুতে ঝুড়ি খালি।

  1. ProductCard@clickসন্তান
  2. emit'add'চিরকুট
  3. ProductList@addবাবা
  4. Piniacart.addstore
  5. App headercart.countশুধু পাঠক
ProductCard.vueMiniket chal, ৳420
<button @click="emit('add', props.product)">যোগ</button>
cart.lines = []   cart.count = 0
Eventনাম আর payload
name:    'add'
payload: { id: 1, name: 'Miniket chal 5kg', price: 420, stock: 12 }
// defineEmits-এর type এটি যাচাই করে
ProductList.vueবাবা শোনে
<ProductCard ... @add="cart.add">
// Vue payload-টি cart.add(product)-এ দেয়
stores/cart.tsমান সরাসরি বদলায়
lines.value.push({ id: 1, price: 420, qty: 1 })
cart.count: 0 → 1   // computed আবার হিসাব হলো
কে আবার render হলোcount-এর পাঠক
App.vue      ঝুড়ি: 0 → 1      render
ProductList  cart.count পড়ে না     render নেই
ProductCard  props বদলায়নি          render নেই
  1. ক্লিক। ক্রেতা Miniket chal-এর “যোগ” চাপলেন। ProductCard নিজে ঝুড়ি চেনে না, তার কাছে শুধু product prop. ঝুড়িতে এখনো ০টি পণ্য।
  2. Emit. emit('add', props.product) একটি event পাঠায়, সঙ্গে পণ্যটি। defineEmits-এ লেখা type-এর জন্য TypeScript ভুল payload ধরবে। এই event DOM-এর event নয়, ওপরে bubble-ও করে না, শুধু সরাসরি বাবার কাছে যায়।
  3. বাবা ধরে। ProductList-এর template-এ @add="cart.add" লেখা। তাই Vue পণ্যটি নিয়ে cart.add ডাকে। React-এ এটি হতো onAdd={add}, একই নকশা।
  4. Store বদলায়। Pinia store-এ lines একটি ref, তাই push সরাসরি চলে। Immer লাগে না, কারণ Proxy নিজেই বদলটা ধরে। count একটি computed, এখন ১.
  5. শুধু পাঠক। Header-এর badge cart.count পড়ে, তাই শুধু App আবার render হয়ে “ঝুড়ি: 1” দেখায়। তালিকা আর কার্ডগুলো কিছু করে না।

v-model: দুই দিকের বাঁধন

পর্ব ১২-এ React-এর controlled input দেখেছেন: value={search} আর onChange={e => setSearch(e.target.value)}. Vue এই জোড়াটিকে একটি শব্দে বাঁধে: v-model="search". একে বলে two-way binding. মান বদলালে input বদলায়, input-এ লিখলে মান বদলায়। এটি জাদু নয়। Compiler এটিকে আসলে একটি value আর একটি input event-এ ভেঙে দেয়। নিজের component-এ v-model চাইলে Vue 3.4 থেকে defineModel() আছে।

নিচে একটি শেখানোর মডেল, সাধারণ DOM আর Proxy দিয়ে v-model-এর ধারণা। Vue লোড হয়নি। Input-এ লিখে দেখুন, আর “খালি করুন” চাপুন। দুই দিকেই বদল দেখা যায়।

<input data-model="search" placeholder="খুঁজুন">
<button id="clear">খালি করুন</button>
<p>খুঁজছেন: <b data-text="search"></b></p>
<script>
// শেখানোর মডেল: v-model-এর ধারণা, আসল Vue নয়
const watchers = {};
const state = new Proxy({ search: '' }, {
  set(t, k, v) {
    t[k] = v;
    (watchers[k] || []).forEach((fn) => fn(v));
    return true;
  },
});
const on = (k, fn) => { (watchers[k] ||= []).push(fn); fn(state[k]); };
document.querySelectorAll('[data-text]').forEach((el) =>
  on(el.dataset.text, (v) => { el.textContent = v; }));
document.querySelectorAll('[data-model]').forEach((el) => {
  const k = el.dataset.model;
  on(k, (v) => { if (el.value !== v) el.value = v; });          // state থেকে input
  el.addEventListener('input', () => { state[k] = el.value; }); // input থেকে state
});
on('search', (v) => console.log('search =', JSON.stringify(v)));
document.getElementById('clear').onclick = () => { state.search = ''; };
const box = document.querySelector('[data-model]');
box.value = 'dal'; box.dispatchEvent(new Event('input'));   // টাইপ করার মতো
state.search = 'chal';                                        // কোড থেকে বদল
</script>

পাতা খোলার সময় log-এ আসে:

search = ""
search = "dal"
search = "chal"

প্রথম লাইন শুরুর মান। দ্বিতীয়টি এসেছে input থেকে state-এ: শেষের আগের লাইনটি টাইপ করার মতো একটি input event পাঠিয়েছে। তৃতীয়টি উল্টো দিকে: কোড সরাসরি state.search লিখেছে, আর input-এর লেখাও “chal” হয়ে গেছে। দুই দিকের এই সংযোগই v-model. if (el.value !== v) পরীক্ষাটি জরুরি। এটি না থাকলে টাইপ করার মাঝখানে cursor শেষে লাফিয়ে যেতে পারে।

Slot: ফাঁকা জায়গায় বাবার markup

ProductCard-এ <slot name="badge" /> একটি ফাঁকা খোপ। ProductList সেখানে <template #badge> দিয়ে নিজের “স্টক নেই” লেখাটি বসায়। React-এ একই কাজ হয়েছে badge নামের একটি JSX prop দিয়ে, যাকে কোর্সে slot prop বলা হয়েছে। নাম ছাড়া <slot /> হলো React-এর children. Vue-তে scoped slot নামে আরেকটি ধরন আছে, যেখানে সন্তান slot-কে data ফেরত দেয়, অনেকটা React-এর render prop-এর মতো।

v-if, v-show আর v-for-এর key

  • v-if: শর্ত মিথ্যা হলে element DOM-এ থাকেই না। React-এর &&-এর মতো।
  • v-show: element সবসময় থাকে, শুধু CSS-এ display: none হয়। ঘন ঘন খোলা-বন্ধ হলে এটি সস্তা। React-এ এর সরাসরি জোড়া নেই, style দিয়ে নিজে করতে হয়।
  • v-for আর :key: key-এর কাজ React-এর মতোই। তালিকা থেকে কোনো পণ্য সরালে বা ক্রম বদলালে কোন node কোন পণ্যের, Vue তা key দিয়ে চেনে। Index-কে key বানালে input-এর লেখা ভুল সারিতে চলে যাওয়ার সেই পুরোনো bug এখানেও ঘটে।
  • এক element-এ v-if আর v-for একসাথে নয়: Vue 3-এ v-if আগে চলে, তাই সে loop-এর variable দেখতে পায় না। আগে computed-এ filter করুন, যেমন বাজারঘরের shown.

৭ · Pinia আর Vue Router

পর্ব ১৮-এ Zustand আর Redux Toolkit দেখেছেন। Vue-র সরকারি store হলো Pinia, যা আগের Vuex-এর জায়গা নিয়েছে। Pinia-র “setup store” আসলে একটি composable: ভেতরে সেই একই ref আর computed.

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { Product } from '../products';

export const useCart = defineStore('cart', () => {
  const lines = ref<{ id: number; price: number; qty: number }[]>([]);
  const count = computed(() => lines.value.reduce((n, l) => n + l.qty, 0));
  function add(p: Product) {
    const line = lines.value.find((l) => l.id === p.id);
    if (line) line.qty++;
    else lines.value.push({ id: p.id, price: p.price, qty: 1 });
  }
  return { lines, count, add };
});
import { create } from 'zustand';
import type { Product } from './products';

type Line = { id: number; price: number; qty: number };
type Cart = { lines: Line[]; add: (p: Product) => void };

export const useCart = create<Cart>((set) => ({
  lines: [],
  add: (p) =>
    set((s) => {
      const line = s.lines.find((l) => l.id === p.id);
      if (!line) return { lines: [...s.lines, { id: p.id, price: p.price, qty: 1 }] };
      return { lines: s.lines.map((l) => (l.id === p.id ? { ...l, qty: l.qty + 1 } : l)) };
    }),
}));

লাইন সংখ্যা সমান (১৩), কিন্তু ভাবনা আলাদা। Zustand-এ নতুন array আর নতুন object বানাতে হয়, কারণ React reference মিলিয়ে বদল চেনে। Pinia-তে line.qty++ সরাসরি লেখা যায়, কারণ Proxy নিজেই লেখা ধরে। আর Pinia-তে selector লাগে না। Component যা পড়ে, শুধু তাতেই নির্ভর করে।

Pinia-র destructure ফাঁদ: const { count } = useCart() লিখলে count সেই মুহূর্তের একটি সাধারণ সংখ্যা হয়ে যায়, আর কখনো বদলায় না। State বা computed আলাদা করতে storeToRefs(cart) ব্যবহার করুন। Function (add) সরাসরি নেওয়া যায়।

Vue Router-এর ধারণাও পর্ব ২০-এর React Router-এর মতো। Route-এর তালিকা, <RouterView> হলো React-এর <Outlet>, <RouterLink> হলো <Link>, আর useRoute().params হলো useParams(). বাজারঘরের দুই পাতার আসল router:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import ProductList from './pages/ProductList.vue';
import ProductPage from './pages/ProductPage.vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: ProductList },
    { path: '/products/:id', component: ProductPage },
  ],
});

createApp(App).use(createPinia()).use(router).mount('#app');

Vue Router 5-এর একটি খবর: আগে আলাদা plugin-এ থাকা file-ভিত্তিক routing এখন মূল package-এ এসেছে। নথি অনুযায়ী যাঁরা file routing ব্যবহার করেন না, তাঁদের জন্য version 4 থেকে 5-এ বড় কোনো ভাঙন নেই। এখানে হাতে লেখা তালিকাই রাখা হয়েছে, কারণ Nuxt-এ file routing আসছে একটু পরেই।

৮ · Nuxt — file routing, useFetch, server/api আর rendering

Vue একা একটি UI library-র মতো। Server rendering, file দিয়ে route আর backend-এর API একসাথে চাইলে লাগে একটি meta-framework. React-এর জন্য যেমন Next.js, Vue-র জন্য তেমন Nuxt. Nuxt প্রথম আসে ২০১৬ সালে, Next.js-এর অল্প পরেই, আর এর পেছনে Chopin ভাইদের নাম প্রচলিত। Nuxt 4 এসেছে ২০২৫-এর জুলাইয়ে। লেখার সময়ে stable version 4.6.3.

$ npm create nuxt@4.0.1 bazarghar-nuxt -- --template minimal --packageManager npm --gitInit=false --no-install
$ cd bazarghar-nuxt && npm install

Nuxt 4-এ অ্যাপের কোড থাকে app/ ফোল্ডারে, আর server-এর কোড server/-এ। ফাইলের নামই route. বাজারঘরের পুরো গঠন:

bazarghar-nuxt/
├─ app/
│  ├─ app.vue                  ← header + <NuxtPage />
│  ├─ pages/
│  │  ├─ index.vue             ← /
│  │  └─ products/[id].vue     ← /products/1, /products/2 …
│  ├─ components/ProductCard.vue
│  └─ composables/useCart.ts
├─ server/
│  ├─ api/products/index.get.ts   ← GET /api/products
│  ├─ api/products/[id].get.ts    ← GET /api/products/:id
│  └─ utils/products.ts
└─ nuxt.config.ts

পর্ব ৫১-এর Next.js-এ যেমন app/products/[id]/page.tsx, Nuxt-এ তেমন pages/products/[id].vue. File-based routing-এর ধারণা এক, নামের নিয়ম আলাদা। আর server/api-র প্রতিটি ফাইল একটি server route, যা চালায় Nuxt-এর server engine Nitro. Next.js-এর route handler-এর মতো, যা পর্ব ৫৭-এ দেখেছেন।

export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'));
  const product = products.find((p) => p.id === id);
  if (!product) throw createError({ statusCode: 404, statusMessage: 'Product not found' });
  return product;
});
<script setup lang="ts">
const route = useRoute();
const { add } = useCart();
const { data: product, error } = await useFetch(`/api/products/${route.params.id}`);
</script>

<template>
  <article v-if="product">
    <h1>{{ product.name }}</h1>
    <p>দাম: ৳{{ product.price }} · মজুত: {{ product.stock }}</p>
    <button :disabled="product.stock === 0" @click="add(product)">ঝুড়িতে যোগ</button>
  </article>
  <h1 v-else-if="error">পণ্যটি পাওয়া যায়নি ({{ error.statusCode }})</h1>
</template>
type Line = { id: number; price: number; qty: number };

export function useCart() {
  const lines = useState<Line[]>('cart', () => []);
  const count = computed(() => lines.value.reduce((n, l) => n + l.qty, 0));
  function add(p: { id: number; price: number }) {
    const line = lines.value.find((l) => l.id === p.id);
    if (line) line.qty++;
    else lines.value.push({ id: p.id, price: p.price, qty: 1 });
  }
  return { count, add };
}

লক্ষ করুন, কোনো ফাইলে import নেই। ref, computed, useFetch, useRoute, এমনকি নিজের useCart আর ProductCard, সব Nuxt নিজে যোগ করে। একে বলে auto-import. Server-এর products-ও server/utils থেকে আপনা থেকে আসে। এর সুবিধা-অসুবিধা একটু পরে।

useFetch আর useAsyncData: একবার আনা, দুবার নয়

Server rendering-এ একটি পুরোনো সমস্যা আছে, যা পর্ব ৫৬-এ দেখেছেন। একই component দুই জায়গায় চলে: প্রথমে server-এ HTML বানাতে, তারপর browser-এ hydration-এর জন্য। setup-এ সরাসরি $fetch লিখলে API দুবার ডাকা হবে। useFetch এটি ঠেকায়। Server-এ আনা ফল HTML-এর ভেতরে payload হিসেবে যায়, আর browser সেখান থেকে পড়ে, আবার আনে না। যেকোনো async function-এর জন্য একই কাজ করে useAsyncData. ক্লিক বা form-এর মতো ঘটনায় সাধারণ $fetch-ই ঠিক, কারণ সেটি শুধু browser-এ চলে।

আগে শেখানোর মডেল, শুধু Node-এর built-in দিয়ে। এখানে makeUseFetch নিয়মটুকু করে: payload-এ থাকলে আর আনবে না।

// শেখানোর মডেল: payload কীভাবে দুবার আনা ঠেকায়, আসল Nuxt নয়
const http = require('node:http');
const products = [{ id: 1, name: 'Miniket chal 5kg' }, { id: 2, name: 'Masur dal 1kg' }];
let apiHits = 0, base;
const getJson = (path) => fetch(base + path).then((r) => r.json());
function makeUseFetch(payload) {                 // payload-এ থাকলে আর আনে না
  return async (key) => (key in payload ? payload[key] : (payload[key] = await getJson(key)));
}
const server = http.createServer(async (req, res) => {
  if (req.url === '/api/products') { apiHits++; return res.end(JSON.stringify(products)); }
  const payload = {};                            // প্রতি request-এর নিজের payload
  const list = await makeUseFetch(payload)('/api/products');
  const html = `<ul>${list.map((p) => `<li>${p.name}</li>`).join('')}</ul>`;
  res.end(`${html}<script id="payload">${JSON.stringify(payload)}</script>`);
});
server.listen(0, '127.0.0.1', async () => {
  base = `http://127.0.0.1:${server.address().port}`;
  const page = await (await fetch(base + '/')).text();
  console.log('HTML:', page.slice(0, 45) + '...');
  console.log('API hits after server render:', apiHits);
  // browser-এ hydration: HTML-এর ভেতরের payload পড়ে
  const sent = JSON.parse(page.match(/id="payload">(.*)<\/script>/)[1]);
  const again = await makeUseFetch(sent)('/api/products');
  console.log('API hits after hydration    :', apiHits, '| products:', again.length);
  // ভুল পথ: setup-এ সরাসরি fetch, যা browser-এ আবার চলে
  await getJson('/api/products');
  console.log('API hits with a plain fetch :', apiHits);
  server.close();
});

আউটপুট:

HTML: <ul><li>Miniket chal 5kg</li><li>Masur dal 1k...
API hits after server render: 1
API hits after hydration    : 1 | products: 2
API hits with a plain fetch : 2

Server render-এ API একবার ডাকা হলো, আর HTML-এ পণ্যের নাম আগেই চলে এলো। Hydration-এর সময় data payload থেকে পড়া হলো, তাই সংখ্যা ১-এই থাকল। শেষের সাধারণ fetch সংখ্যাটি ২ করল, যা payload ছাড়া প্রতি পাতা খোলায় ঘটত। সাবধান: এই মডেল JSON.stringify সরাসরি <script>-এ বসায়। কোনো পণ্যের নামে </script> থাকলে HTML ভেঙে XSS হতে পারে। আসল Nuxt একটি বিশেষ serializer দিয়ে এই অক্ষরগুলো escape করে। নিজে এমন কিছু বানালে XSS-এর পর্বটি আগে দেখে নিন।

এবার আসল Nuxt. সত্যিকারের run: nuxt build, তারপর node .output/server/index.mjs একটি local port-এ, আর Edge-এ Playwright দিয়ে network দেখা। একটি পাতার অনুরোধ পুরো পথে:

  1. BrowserGET /প্রথম পাতা
  2. NitrouseFetchserver-এ setup
  3. HTML__NUXT_DATA__data সহ
  4. Hydrationpayloadআবার আনা নয়
  5. NuxtLink/products/1client-এ
Browserখালি cache
GET /
// এখনো কোনো JavaScript নেই
Server-এ index.vue-র setupNitro log
await useFetch('/api/products')
// server-এর ভেতরেই API route ডাকা হলো, network ছাড়া
[server] GET /api/products
Response HTML3176 bytes
<li class="card"><a href="/products/1">Miniket chal 5kg</a><span>৳420</span>…
<script type="application/json" id="__NUXT_DATA__">
[…,"Miniket chal 5kg",420,12,…,"Masur dal 1kg",135,0,…]
Browser-এর networkPlaywright
script /_nuxt/Ce0dpAcg.js  …  ৬টি JS ফাইল
fetch /api/products: নেই
// useFetch payload থেকে পড়ল
Link-এ ক্লিকএবার browser আনে
fetch /api/products/1   57 bytes
নতুন HTML নেই, পাতা বদলায় client-এ
ঝুড়িতে যোগ → ঝুড়ি: 1
  1. প্রথম অনুরোধ। Browser / চাইল। এই মুহূর্তে তার কাছে কোনো JavaScript নেই।
  2. Server-এ setup. Nitro index.vue-র <script setup> চালাল। useFetch server-এর ভেতরেই API route ডাকল, তাই server log-এ একটি লাইন এলো। await থাকায় data না আসা পর্যন্ত render অপেক্ষা করল।
  3. Data সহ HTML. ৬টি পণ্যসহ পুরো তালিকা HTML-এ এলো, যা search engine আর ধীর ফোন দুজনেরই কাজে লাগে। একই data __NUXT_DATA__ নামের একটি JSON script-এ বসল।
  4. Hydration. Browser ৬টি JS ফাইল নামাল, আর component-গুলো আবার চালাল। এবার useFetch payload-এ data পেল, তাই /api/products-এ কোনো network অনুরোধ যায়নি। Playwright-এর তালিকায় সত্যিই কোনো /api/products নেই।
  5. Client navigation. পণ্যে ক্লিক করলে Nuxt নতুন HTML চায় না। Browser নিজেই /api/products/1 থেকে ৫৭ byte JSON আনল আর পাতা বদলাল। এরপর “ঝুড়িতে যোগ” চাপলে badge 1 হলো।

Build-এর আসল আউটপুট (লম্বা path-এর শুরু “…” দিয়ে ছোট করা, server-এর বেশিরভাগ লাইন বাদ):

$ npm run build
● Nuxt 4.6.3 (with Nitro 2.13.6, Vite 8.3.5 and Vue 3.5.44)
● Nitro preset: node-server
ℹ vite v8.3.5 building client environment for production...
✓ 182 modules transformed.
…/client/_nuxt/8sweXEme.js     1.53 kB │ gzip:  0.94 kB
…/client/_nuxt/Bi0CVc-0.js    12.04 kB │ gzip:  4.57 kB
…/client/_nuxt/DWpamHSj.js    28.70 kB │ gzip: 11.67 kB
…/client/_nuxt/t4csLo-a.js    68.67 kB │ gzip: 25.56 kB
…/client/_nuxt/Ce0dpAcg.js    92.94 kB │ gzip: 35.61 kB
…
[nitro] ✔ Nuxt Nitro server built
  ├─ .output/server/chunks/routes/api/index.get.mjs (553 B) (279 B gzip)
  ├─ .output/server/chunks/routes/api/products/_id_.get.mjs (749 B) (371 B gzip)
  …
Σ Total size: 2.15 MB (547 kB gzip)
[nitro] ✔ You can preview this build using node .output/server/index.mjs
◆ ✨ Build complete in 4.76s!

প্রথম পাতায় browser যে ৬টি JS ফাইল নামাল, Vite-এর হিসাবে তাদের মোট gzip আকার 78.89 kB. Server-এর পুরো .output 2.15 MB, যা browser-এ যায় না। একটি অনুরোধ ভুল id নিয়ে গেলে createError-এর ফল সত্যিকারের run-এ এমন এলো:

GET /api/products/99  →  HTTP 404
{ "error": true, "statusCode": 404, "statusMessage": "Product not found", "message": "Product not found", … }
পাতা /products/99 → h1: পণ্যটি পাওয়া যায়নি (404)

Rendering mode আর route rules

Nuxt-এর default হলো universal rendering: প্রথম পাতা server-এ HTML, তারপর browser-এ SPA. পর্ব ৫৫-এর সব কৌশল এখানে route rules দিয়ে URL ধরে ধরে বেছে নেওয়া যায়। Nuxt-এর নথির উদাহরণের ধাঁচে বাজারঘরের জন্য:

export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },              // build-এর সময় HTML (SSG)
    '/products/**': { swr: 3600 },         // cache, পেছনে নতুন করা
    '/blog/**': { isr: 3600 },             // CDN-এ cache, সমর্থন থাকলে
    '/admin/**': { ssr: false },           // শুধু browser-এ (CSR)
  },
});

এই config-টি নথির ধাঁচে লেখা উদাহরণ, এই পর্বের project-এ চালানো হয়নি। কোন rule কোথায় আসলে কাজ করবে, তা deployment-এর ধরনের ওপরও নির্ভর করে। ISR-এর মতো কিছু rule-এর জন্য hosting-এর সমর্থন লাগে। পুরো অ্যাপ browser-এ চালাতে চাইলে ssr: false.

Auto-import: আরাম আর তার দাম

সুবিধা

ফাইলের মাথায় import-এর লম্বা তালিকা থাকে না। Nuxt type তৈরি করে, তাই editor-এর সাহায্য থাকে। শুধু ব্যবহৃত জিনিসই production build-এ যায়।

দাম

ফাইল পড়ে বোঝা যায় না useCart কোথা থেকে এলো। একই নামের দুটি composable থাকলে বিভ্রান্তি হয়। Nuxt-এর বাইরে কোড সরাতে গেলে সব import হাতে লিখতে হয়।

মাঝামাঝি পথ

#imports থেকে স্পষ্ট import লেখা যায়। imports: { autoImport: false } দিলে পুরোটা বন্ধ হয়। তখন এই স্পষ্ট import-ই একমাত্র পথ।

Server-এ ভাগ হয়ে যাওয়া state: বাজারঘরের ঝুড়ি useState('cart') দিয়ে, কোনো module-এর const lines = ref([]) দিয়ে নয়। কারণ server একটি process-এ সব ক্রেতার request সামলায়। Module-এর একটি ref server-এ সবার মধ্যে ভাগ হয়ে যায়। রফিকের ঝুড়ির পণ্য মিনার পাতায় চলে আসতে পারে। useState প্রতি request-এর জন্য আলাদা state বানায়, আর payload দিয়ে browser-এ পাঠায়। Pinia-ও @pinia/nuxt module দিয়ে একই নিয়ম মানে।

৯ · বাজারঘর তিনভাবে — মাপ, আর কখন Vue বা Nuxt

একই দুই পাতা, তিনটি project: পণ্যের তালিকা (খোঁজা, স্টক নেই লেখা, যোগ বোতাম) আর পণ্যের পাতা, সঙ্গে ঝুড়ির সংখ্যা। Vue আর React দুটোই create-vite 9.3.3 থেকে, router আর store সহ। পণ্যের ৯ লাইনের ডেটা-ফাইল তিনটিতেই এক, তাই লাইনের হিসাবে ধরা হয়নি।

# ~/vue-shop  (Vue 3.5.44, Vue Router 5.4.0, Pinia 4.1.0)
$ npm run build
✓ 30 modules transformed.
dist/assets/index-BfXAcdUC.js   98.47 kB │ gzip:  38.48 kB

# ~/react-shop  (React 19.3.1, React Router 8.5.1, Zustand 5.0.16)
$ npm run build
✓ 100 modules transformed.
dist/assets/index-BnuNcq42.js  316.66 kB │ gzip: 100.05 kB

# শুধু framework, একটি counter বোতাম (একই project-এ আলাদা entry)
Vue:   dist-min/assets/min-CusSLt7K.js   61.96 kB │ gzip: 24.38 kB
React: dist-min/assets/min-BWHS8rFc.js  219.55 kB │ gzip: 68.61 kB
মাপ (একটি local run)Vue + ViteReact + ViteNuxt
শুধু framework, gzip24.38 kB68.61 kB—
পুরো অ্যাপের JS, gzip38.48 kB100.05 kB78.89 kB (প্রথম পাতায় ৬ ফাইল)
Framework-এর ওপরে বাড়তি (router, store, অ্যাপ), gzipপ্রায় 14.1 kBপ্রায় 31.4 kBNuxt-এর ভেতরে
অ্যাপের কোড, ফাঁকা বাদে লাইন9110067 + server API 10
Router-এর তালিকা হাতে লেখাহ্যাঁহ্যাঁনা, ফাইলের নাম
প্রথম HTML-এ পণ্যের নামনানাহ্যাঁ (SSR)
নিজের APIনেইনেইআছে (server/api)

সংখ্যাগুলো সতর্ক হয়ে পড়ুন। এগুলো একটি ছোট demo-র একটি local run, সাধারণ সিদ্ধান্ত নয়। React-এর দিকে বড় অংশটি React DOM আর React Router 8, যা loader, action, data API-র মতো অনেক কিছু নিয়ে আসে। পর্ব ২০-এর পুরো ক্ষমতা এখানে ব্যবহারই হয়নি। Nuxt-এর ৭৮.৮৯ kB-এর সাথে Vue SPA-র ৩৮.৪৮ kB সরাসরি তুলনীয়ও নয়। Nuxt-এ আছে hydration, payload, head ব্যবস্থাপনা আর error পাতা, আর বিনিময়ে প্রথম HTML-এই data চলে আসে। বড় অ্যাপে framework-এর এককালীন আকারের চেয়ে আপনার নিজের কোড আর library দ্রুত বড় হয়।

নতুন কাজ, সাবধানে: Vue 3.6-এর release candidate-এর changelog অনুযায়ী এতে Vapor Mode আছে: ঐচ্ছিক একটি compile পদ্ধতি, যা virtual DOM ছাড়াই সরাসরি DOM বদলানোর কোড বানায়, Solid বা Svelte-এর মতো। Changelog বলছে এটি Vue API-র একটি অংশ সমর্থন করে, যেমন Options API সমর্থন করে না। একই release-এ reactivity-র ভেতরের অংশ alien-signals নামের একটি library-র ধারণায় নতুন করে লেখা হয়েছে। লেখার সময়ে এগুলো stable নয়, তাই production-এ নেওয়ার আগে তখনকার সরকারি নথি দেখুন।

কখন Vue বা Nuxt বেছে নেবেন

পরিস্থিতিযেদিকে ঝুঁকতে পারেন
দলে HTML আর CSS জানা লোক বেশি, JSX অপরিচিতVue, কারণ template HTML-এর মতো
পুরোনো server-rendered সাইটে (Laravel, Django) একটু একটু করে interactivityVue, কারণ একটি পাতার একটি অংশে সহজে বসানো যায়
SEO-নির্ভর দোকান বা কনটেন্ট সাইট, দল Vue জানেNuxt
দল আগে থেকেই React জানে, বা React Native-এ mobile অ্যাপও লাগবেReact, Next.js
খুব বড় তৃতীয় পক্ষের library আর নিয়োগের বাজার জরুরিনিজের অঞ্চলের চাকরির বিজ্ঞাপন আর দলের অভিজ্ঞতা যাচাই করুন
“কোনটি দ্রুত” একমাত্র প্রশ্ননিজের অ্যাপে মাপুন; ছোট পার্থক্য সিদ্ধান্ত বদলায় না

Community-তে এ নিয়ে মতভেদ আছে। কেউ বলেন Vue শেখা সহজ আর কম ভুলের সুযোগ দেয়। কেউ বলেন React-এর ecosystem বড় বলে সেটি বেশি সুবিধার। দুই দিকেরই যুক্তি আছে, আর বাজারের অবস্থা দেশ আর সময়ভেদে বদলায়। Laravel-এর সাথে Vue-র একটি পুরোনো যোগ আছে: Laravel-এর starter kit-এ বহুদিন ধরে Vue একটি বিকল্প, যা পর্ব ৭০-এ Laravel দেখার সময় কাজে লাগবে। কোন framework-এর চাকরি বেশি, তার নির্ভরযোগ্য সংখ্যা এখানে দেওয়া হচ্ছে না; নিজের শহরের বিজ্ঞাপন দেখে নিন। একটি framework ভালো করে জানলে অন্যটি শিখতে কয়েক সপ্তাহ লাগে, কারণ component, state, effect আর routing-এর ধারণা একই।

আরও গভীরে: Vue-র compiler template থেকে render function বানানোর সময় প্রতিটি dynamic node-এ একটি “patch flag” বসায়, যেমন “শুধু text বদলাতে পারে” বা “শুধু class”. Static অংশগুলো একবার তৈরি করে বারবার ব্যবহার করা হয়, আর dynamic node-গুলো একটি সমতল “block” তালিকায় থাকে। তাই update-এর সময় পুরো tree না ঘুরে শুধু এই তালিকা দেখা হয়। Vue-র নথিতে একে “compiler-informed virtual DOM” বলা হয়েছে। React Compiler উল্টো দিক থেকে একই লক্ষ্যে যায়: সে JavaScript বিশ্লেষণ করে memoization যোগ করে, যাতে অকারণ render কমে।

পরীক্ষা ও ইন্টারভিউতে: Vue-তে reactivity কীভাবে কাজ করে? Proxy-র get-এ track, set-এ trigger; শুধু নির্ভরশীল effect আবার চলে। ref আর reactive-এর পার্থক্য? ref যেকোনো মানের বাক্স, .value লাগে; reactive শুধু object, destructure করলে reactivity হারায়। computed আর watch? computed cache-সহ মান ফেরায়, watch বদলের পর side effect চালায়। Nuxt-এ useFetch কেন, $fetch কেন নয়? Server-এর ফল payload-এ যায়, তাই hydration-এ দুবার আনা হয় না। Vue-তে v-if আর v-show? প্রথমটি DOM থেকে সরায়, দ্বিতীয়টি শুধু লুকায়।

১০ · AI-এর যুগে Vue আর Nuxt

Vue 2 আর Nuxt 2-এর কোড ইন্টারনেটে অনেক বছর ধরে জমেছে। তাই AI প্রায়ই পুরোনো ধাঁচের কোড দেয়, যা আজকের version-এ চলে না বা অন্যভাবে চলে। কোড চললেও সেটি এখনকার পথ নাও হতে পারে।

AI-এর ভুলকীভাবে ধরবেন
new Vue({ el: '#app' }), Vue.component, filterএগুলো Vue 2-এর। Vue 3-এ createApp
নতুন project-এ VuexVue-র সরকারি সুপারিশ এখন Pinia
Nuxt-এ asyncData() option বা pages/ root-এNuxt 2-এর ধাঁচ। Nuxt 4-এ useFetch আর app/pages/
setup-এ await $fetch(...)Network tab খুলুন: API দুবার ডাকা হচ্ছে কি না
Module-এর ref-এ ব্যবহারকারীর ডেটা, SSR-এদুই browser-এ দুইজন হয়ে চেষ্টা করুন, ডেটা মিশছে কি না
Secret runtimeConfig.public-এpublic-এর সব কিছু browser-এ যায়। Secret শুধু server-এর অংশে
এক element-এ v-if আর v-forVue-র eslint plugin এটি ধরে; computed-এ filter করুন
নিজে বানানো option বা নেই এমন composableআপনার version-এর সরকারি docs খুলুন, বা nuxt build চালান

AI-কে কোড লেখাতে গেলে version, সীমা আর যাচাই একসাথে বলে দিন:

আমি Nuxt 4 (Vue 3.5, TypeScript) দিয়ে একটি অনলাইন দোকান বানাচ্ছি। app/ ফোল্ডারের গঠন ব্যবহার করছি। ১. পণ্যের পাতা app/pages/products/[id].vue-তে লেখো, data আনবে useFetch দিয়ে, setup-এ সরাসরি $fetch নয়। ২. API থাকবে server/api/products/[id].get.ts-এ, ভুল id হলে createError দিয়ে 404 ফেরত দাও। ৩. ঝুড়ির state module-এর ref-এ নয়, useState বা @pinia/nuxt-এ রাখো, আর কারণ বলো। ৪. কোনো secret runtimeConfig.public-এ রাখবে না। ৫. Vue 2, Nuxt 2 বা Options API-র কোড দিও না। নতুন package যোগ করার আগে কারণ বলো। ৬. শেষে বলো, আমি কীভাবে যাচাই করব যে hydration-এর সময় API দুবার ডাকা হচ্ছে না। জানা না থাকলে অনুমান কোরো না, প্রশ্ন করো।

AI-এর লেখা Vue বা Nuxt কোড হাতে পেলে

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Vue 2 থেকে Vue 3-এ reactivity কেন নতুন করে লেখা হলো? এই বদলের দাম কে দিল?

Vue 2-এর reactivity দাঁড়িয়ে ছিল Object.defineProperty-এর ওপর। এই পদ্ধতিতে Vue শুরুতে একটি object-এর প্রতিটি property-কে getter আর setter দিয়ে মুড়ে দিত। কাজ চলত, কিন্তু তিনটি সমস্যা ছিল। এক: পরে যোগ করা নতুন property Vue দেখতে পেত না, কারণ মোড়ানো হয়েছিল শুরুতেই। তাই Vue.set নামে একটি বাড়তি function মনে রাখতে হতো। দুই: Array-র index ধরে লেখা, যেমন list[3] = x, ধরা পড়ত না। Vue array-র push-এর মতো method-গুলো নিজে বদলে এই সমস্যার একটি অংশ ঢাকত। তিন: বড় object শুরুতেই পুরোটা ঘুরে মোড়াতে হতো, যা ধীর।

ES2015-এর Proxy এই তিনটিই সমাধান করে। Proxy পুরো object-এর সামনে বসে, তাই নতুন property, মুছে ফেলা, এমনকি in দিয়ে খোঁজাও ধরা যায়। আর মোড়ানো হয় অলসভাবে, শুধু যে অংশ পড়া হয় তখন। Vue 3 (২০২০) এই পথ নিল।

দাম দিল পুরোনো browser. Proxy-র আচরণ polyfill দিয়ে পুরোপুরি নকল করা যায় না, তাই Vue 3 Internet Explorer 11 সমর্থন করে না। যে সব প্রতিষ্ঠানের ব্যবহারকারী তখনো IE11-এ ছিলেন, তাঁদের জন্য Vue দল Vue 2-এর জীবন বাড়াল আর ২০২২-এ Vue 2.7 আনল, যাতে Composition API-র একটি অংশ পুরোনো ব্যবস্থাতেই পাওয়া যায়। শেষ পর্যন্ত Vue 2-এর জীবন শেষ হলো ২০২৩-এর শেষে।

আরেকটি দাম দিলেন ডেভেলপাররা। Vue 2 থেকে 3-এ যাওয়া অনেক দলের জন্য কঠিন ছিল, বিশেষ করে যাঁরা এমন library-র ওপর নির্ভর করতেন যা দ্রুত Vue 3-এ আসেনি। এই অভিজ্ঞতা থেকে একটি শিক্ষা আছে, যা React-এর জন্যও সত্য: framework-এর বড় version বদল কয়েক বছরের প্রকল্প, আর ecosystem-এর গতি framework-এর গতির চেয়ে ধীর।

মূল উপলব্ধি: Platform-এ নতুন ক্ষমতা (Proxy) এলে পুরোনো ঝামেলার সমাধান হয়, কিন্তু পুরোনো ব্যবহারকারী আর পুরোনো কোডকে সঙ্গে নিতে বছরের পর বছর লাগে। প্রযুক্তি বাছাইয়ে “পরের বড় version কতটা কষ্ট দেবে” প্রশ্নটিও রাখুন।

প্র ০২Template বনাম JSX: কোনটি “ভালো”? Compiler-এর দিক থেকে ভাবুন।

এই তর্ক প্রায় দশ বছরের পুরোনো, আর অনেক সময় রুচির লড়াইয়ে আটকে থাকে। প্রকৌশলের দিক থেকে প্রশ্নটি অন্যভাবে করা যায়: build-এর সময় কতটা জানা যায়?

Vue-র template একটি সীমিত ভাষা। v-if, v-for, {{ }}-এর বাইরে বেশি কিছু নেই। ঠিক এই সীমার জন্য compiler নিশ্চিত হতে পারে কোন অংশ কখনো বদলাবে না। ধরুন বাজারঘরের header-এ “বাজারঘর” লেখা আর একটি লোগো আছে, আর পাশে ঝুড়ির সংখ্যা। Compiler জানে প্রথম দুটি স্থির, শুধু সংখ্যার text বদলায়। তাই update-এ শুধু সেই একটি text node দেখা হয়। একে Vue-র নথি বলে compiler-informed virtual DOM.

JSX পুরো JavaScript. এক লাইনে একটি function ডাকা যায়, যা কখন কী ফেরাবে, compiler আগে থেকে জানে না। তাই React বহুদিন runtime-এ পুরো subtree মেলাত, আর অপ্টিমাইজেশনের দায় দিত memo, useMemo আর ডেভেলপারের হাতে। এর লাভ হলো স্বাধীনতা: শর্ত, loop, helper function, সবই সাধারণ JavaScript, নতুন ভাষা শিখতে হয় না, আর TypeScript-এর সাহায্যও সরাসরি পাওয়া যায়।

মজার ব্যাপার হলো দুই দিক কাছে আসছে। React Compiler JSX-কে বিশ্লেষণ করে memoization বসায়, অর্থাৎ build-এর সময় বেশি জানার চেষ্টা। অন্যদিকে Vue JSX সমর্থন করে, আর তার নতুন Vapor Mode template থেকে আরও সরাসরি DOM-এর কোড বানানোর পথে যাচ্ছে। Svelte আর Solid-ও compiler-কে কেন্দ্রে রেখেছে, যা পরের পর্বে দেখবেন।

দলের দিক থেকেও প্রশ্ন আছে। যাঁরা HTML আর CSS থেকে এসেছেন, তাঁদের কাছে template পরিচিত লাগে। যাঁরা JavaScript-এ দক্ষ, তাঁরা JSX-এ বেশি স্বচ্ছন্দ। কোনোটিই অন্যটিকে বাতিল করে না।

মূল উপলব্ধি: ভাষার সীমা আসলে compiler-এর জন্য তথ্য। যত বেশি স্বাধীনতা, তত কম অনুমান; যত বেশি নিয়ম, তত বেশি স্বয়ংক্রিয় অপ্টিমাইজেশন। দুটির মধ্যে বাছাই একটি trade-off, সত্য-মিথ্যার প্রশ্ন নয়।

প্র ০৩Signal আর fine-grained reactivity এখন প্রায় সব framework-এ আসছে। React কেন এখনো এই পথে যায়নি?

গত কয়েক বছরে fine-grained reactivity প্রায় একটি ঢেউয়ের মতো ছড়িয়েছে। SolidJS শুরু থেকেই signal-এর ওপর দাঁড়ানো। Angular ২০২৩ সালে signal আনল। Svelte 5 “rune” দিয়ে compiler-নির্ভর reactivity থেকে signal-ভিত্তিক runtime-এ গেল। Preact-এর আলাদা signals package আছে। JavaScript-এ নিজেই একটি Signals যোগ করার প্রস্তাব TC39-এ আলোচনায় আছে; লেখার সময়ে (নভেম্বর ২০২৬) প্রস্তাবের পাতায় এটি Stage 1, অর্থাৎ প্রাথমিক ধাপে, আর এখনো ভাষার অংশ নয়। Vue এই ধারণা ২০১৪ থেকেই ব্যবহার করছে, শুধু নাম ছিল আলাদা।

React দল প্রকাশ্যে বলেছে তারা UI-কে state-এর একটি function হিসেবে দেখতে চায়: UI = f(state). প্রতি render-এ সব মান নতুন করে পড়া হয়, তাই “কোন মান কোথা থেকে এলো” প্রশ্নের উত্তর সবসময় সরল: এই render-এর snapshot. এতে stale dependency, ভুল subscription বা মান পড়েও ধরা না পড়ার মতো bug কম। Concurrent rendering-ও এই মডেলের ওপর দাঁড়ানো: একটি render বাতিল করে আবার শুরু করা নিরাপদ, কারণ render কোনো কিছু বদলায় না।

Signal-এর পথে লাভ স্পষ্ট: কাজ কম হয়, memo লাগে না। দামও আছে। প্রতিটি মানের পাঠকের তালিকা রাখতে মেমরি লাগে। মান destructure করলে সংযোগ ছিঁড়ে যায়, যা এই পর্বে Pinia আর reactive-এ দেখেছেন। আর mutable data-য় “কে কখন বদলাল” খোঁজা কঠিন হতে পারে, যদিও Vue DevTools এতে সাহায্য করে।

React নিজের সমস্যার উত্তর দিচ্ছে অন্য পথে: React Compiler. অর্থাৎ মডেল একই থাকবে, অকারণ কাজ কমবে build-এর সময়ের বিশ্লেষণে। এটি সফল হলে দুই পথের বাস্তব পার্থক্য অনেক কমে যাবে, যা এই পর্বের মডেলের “React + memo” লাইনে দেখেছেন।

মূল উপলব্ধি: একই সমস্যার (অকারণ কাজ) দুটি সমাধান: data-কে বুদ্ধিমান করা (signal), অথবা compiler-কে বুদ্ধিমান করা (memoization). কোনটি জিতবে, তার চেয়ে জরুরি হলো দুই মডেলের ফাঁদগুলো চেনা।

প্র ০৪Nuxt-এর payload HTML-এর ভেতরে data পাঠায়। এর খরচ আর ঝুঁকি কী কী?

Payload-এর লাভ এই পর্বে মেপে দেখেছেন: API একবার ডাকা হয়, hydration-এ আর নয়। কিন্তু প্রতিটি সুবিধার মতো এরও খরচ আছে, আর বড় অ্যাপে সেগুলো চোখে পড়ে।

আকার: একই data দুবার যায়: একবার HTML-এর লেখা হিসেবে, একবার JSON হিসেবে। বাজারঘরের ৬টি পণ্যে পুরো HTML ছিল ৩১৭৬ byte, সমস্যা নেই। কিন্তু ৫০০টি পণ্যের প্রতিটির বিবরণ, ছবির তালিকা আর রিভিউ পুরোটা আনলে payload কয়েকশো kB হতে পারে। এর প্রতিকার useFetch-এর pick বা transform দিয়ে শুধু দরকারি field রাখা, আর API-তে পর্ব ৩৪-এর pagination.

নিরাপত্তা: Payload-এ যা যায়, ব্যবহারকারী তা দেখতে পারেন। Page source খুললেই __NUXT_DATA__ পড়া যায়। তাই API যদি পণ্যের ক্রয়মূল্য বা অন্য ক্রেতার তথ্য ফেরত দেয়, আর template-এ সেটি না দেখানোও হয়, তবু সেটি ফাঁস হয়েছে। নিয়ম: server route শুধু সেই field ফেরত দেবে যা এই ব্যবহারকারী দেখার অধিকার রাখেন। দ্বিতীয় ঝুঁকি XSS: data-য় </script> থাকলে HTML ভেঙে যেতে পারে। Nuxt এর জন্য বিশেষ serializer ব্যবহার করে, কিন্তু নিজে এমন ব্যবস্থা বানালে এই escape আপনার দায়িত্ব।

Key-র সংঘাত: Nuxt payload-এ data রাখে একটি key দিয়ে। useFetch সাধারণত URL আর অবস্থান থেকে key নিজে বানায়। কিন্তু useAsyncData-এ দুটি আলাদা component একই key ভিন্ন function দিয়ে ব্যবহার করলে একজন আরেকজনের data পেতে পারে। Nuxt-এর নথিও একই key-র call-গুলোর option এক রাখতে বলে।

ভাগ হয়ে যাওয়া state: সবচেয়ে গুরুতর ভুল payload-এর নয়, কিন্তু একই জায়গা থেকে আসে। Server একটি process-এ বহু request চালায়। Module-এর একটি ref-এ ব্যবহারকারীর ঝুড়ি রাখলে সেটি সব request-এ ভাগ হয়, আর একজনের তথ্য আরেকজনের payload-এ চলে যেতে পারে। useState আর Pinia-র Nuxt module প্রতি request-এ আলাদা state দেয়।

মূল উপলব্ধি: Server আর browser-এর সীমানা পার হওয়া সব data-ই প্রকাশ্য, আর server-এর memory সব ব্যবহারকারীর। SSR framework এই সীমানা সহজ করে, অদৃশ্য করে না।

প্র ০৫একটি দল React জানে, কিন্তু নতুন প্রকল্পে Vue প্রস্তাব এসেছে। সিদ্ধান্ত কীভাবে নেবেন?

এমন সিদ্ধান্তে প্রায়ই দুটি ভুল হয়। একটি হলো benchmark দেখে সিদ্ধান্ত নেওয়া। এই পর্বের সংখ্যা দেখলে মনে হতে পারে Vue “অর্ধেক আকারের”, কিন্তু বড় অ্যাপে framework-এর এককালীন আকারের চেয়ে নিজের কোড, ছবি আর তৃতীয় পক্ষের script অনেক বেশি জায়গা নেয়। অন্য ভুলটি হলো জনপ্রিয়তার তালিকা দেখে সিদ্ধান্ত নেওয়া, নিজের দলের অবস্থা না দেখে।

ভালো পথ হলো প্রশ্নের একটি ছক। দল: এখন কারা কোড লিখবেন, আর আগামী দুই বছরে কাদের নিয়োগ করবেন? দলের সবাই React জানলে Vue-তে যেতে কয়েক সপ্তাহের শেখার খরচ আছে, যদিও ধারণা একই। পুরোনো ব্যবস্থা: backend কি Laravel? তাহলে Vue-র সাথে তার পুরোনো যোগাযোগ কাজে লাগতে পারে। Mobile অ্যাপও কি লাগবে? তাহলে React Native-এর কারণে React-এর দিকে যুক্তি জোরালো। প্রয়োজন: SEO কি জরুরি? তাহলে Vue নয়, Nuxt, আর React নয়, Next.js বা অন্য SSR framework. Ecosystem: দরকারি library, যেমন payment SDK, chart বা rich text editor, কি দুই দিকেই আছে?

তারপর একটি ছোট পরীক্ষা চালান। এই পর্বে যেমন বাজারঘরের দুটি পাতা তিনভাবে বানানো হয়েছে, তেমনি আসল প্রকল্পের সবচেয়ে কঠিন একটি পাতা দুই framework-এ এক-দুই দিনে বানান। লাইন, build-এর আকার, আর দলের অনুভূতি লিখে রাখুন। সিদ্ধান্ত ও তার কারণ একটি ছোট নথিতে লিখুন, যাকে অনেকে ADR (architecture decision record) বলেন, যাতে পরের বছর কেউ জিজ্ঞেস করলে উত্তর থাকে।

Vue 2-এর জীবন শেষ হওয়ার ঘটনা থেকে আরেকটি শিক্ষা: যেকোনো framework-এর বড় version বদলের খরচ পরিকল্পনায় রাখুন। আজ যা নেবেন, পাঁচ বছর পরে সেটি আপগ্রেড করতে হবে।

মূল উপলব্ধি: Framework বাছাই একটি দলের সিদ্ধান্ত, শুধু প্রযুক্তির নয়। ধারণা (component, reactivity, routing, SSR) এক জায়গায় শিখলে অন্য framework-এ সেগুলো চিনে নেওয়া সহজ, আর সেটিই এই মডিউলের উদ্দেশ্য।

অনুশীলন

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

  1. Bundle-এর হিসাব: একটি build-এ gzip আকার Vue SPA 38.48 kB, React SPA 100.05 kB, আর Nuxt-এর প্রথম পাতার JS 78.89 kB. ধরুন সংযোগের গতি 1.6 Mbps. (ক) প্রতিটি নামাতে কত মিলিসেকেন্ড লাগবে? (খ) React আর Vue-র পার্থক্য কত মিলিসেকেন্ড? লেটেন্সি আর parse-এর সময় বাদ দিন।

    1.6 Mbps = 1600 kbit/s, আর 1 kB = 8 kbit.

    (ক) Vue: 38.48 × 8 = 307.84 kbit. সময় = 307.84 ÷ 1600 = 0.1924 s ≈ 192.4 ms.

    React: 100.05 × 8 = 800.4 kbit. সময় = 800.4 ÷ 1600 = 0.50025 s ≈ 500.3 ms.

    Nuxt: 78.89 × 8 = 631.12 kbit. সময় = 631.12 ÷ 1600 = 0.39445 s ≈ 394.5 ms.

    (খ) 500.3 − 192.4 ≈ 307.9 ms. ধীর মোবাইল সংযোগে এটি টের পাওয়া যায়। তবে Nuxt-এর ক্ষেত্রে পণ্যের তালিকা JS নামার আগেই HTML-এ দেখা যায়, তাই “কখন প্রথম কিছু দেখা গেল” প্রশ্নে Nuxt এগিয়ে থাকে। একটি সংখ্যা দিয়ে পুরো ছবি পাওয়া যায় না।

  2. ফল অনুমান করুন: একটি Proxy-ভিত্তিক reactivity ব্যবস্থায় তিনটি effect আছে: A পড়ে cart.count, B পড়ে cart.shopper, C পড়ে একটি computed total = cart.count * price.value. মান বদলায় না এমন লেখায় কোনো effect চলে না। শুরুতে count = 2, price = 420. ক্রমে চালানো হলো: price.value = 400, cart.count = 2, cart.count = 5, cart.shopper = 'Rafi' (আগেও Rafi ছিল)। প্রতিটি লেখার পরে কোন কোন effect চলবে, আর শেষে total কত?
    1. price.value = 400: computed আবার হিসাব করে 2 × 400 = 800, তাই C চলে। A, B চলে না।
    2. cart.count = 2: মান আগেও 2, তাই কিছুই চলে না।
    3. cart.count = 5: computed 5 × 400 = 2000, তাই C আর A চলে।
    4. cart.shopper = 'Rafi': একই মান, কিছুই চলে না।

    শেষে total = 2000. B একবারও চলেনি। React-এ একই চারটি setState-এ (সমান মানে React-ও render ছেড়ে দিতে পারে) যে component state রাখে, সে আর তার নিচের সবাই চলত, যদি না memo থাকে।

  3. Code review: AI একটি Nuxt পাতা লিখেছে। এতে আছে: const products = await $fetch('/api/products') সরাসরি <script setup>-এ; <li v-for="p in products" v-if="p.stock > 0">, কোনো :key নেই; server/utils/cart.ts-এ export const cart = ref([]); আর runtimeConfig.public.paymentSecret. অন্তত পাঁচটি সমস্যা ধরুন আর ঠিক করার পথ বলুন।
    • $fetch setup-এ: server-এ একবার, hydration-এ আবার। useFetch('/api/products') দিন।
    • v-for আর v-if এক element-এ: Vue 3-এ v-if আগে চলে, p তখনো নেই। computed-এ filter করুন।
    • :key নেই: তালিকা বদলালে node ভুল পণ্যের সাথে মিলতে পারে। :key="p.id" দিন।
    • Module-এর ref-এ ঝুড়ি: server-এ সব ক্রেতার মধ্যে ভাগ হবে। useState বা @pinia/nuxt, আর আসল ঝুড়ি server-এর database-এ।
    • Secret public-এ: browser-এর bundle-এ চলে যাবে। runtimeConfig-এর server অংশে রাখুন, আর মান আসুক environment variable থেকে (পর্ব ৭৮-এ বিস্তারিত)।
    • বাড়তি: API থেকে পুরো পণ্য না এনে শুধু দরকারি field, যাতে payload ছোট থাকে আর গোপন field ফাঁস না হয়।
  4. নিজে মেপে দেখুন: npm create vite@latest দিয়ে একটি vue-ts আর একটি react-ts project বানান। এই পর্বের Parent, Badge, Name, Static আর Hits লিখুন, প্রতিটিতে render গোনার log রাখুন। npm run build আর npm run preview চালিয়ে “যোগ” তিনবার চাপুন। তারপর React-এ Name আর Static-কে memo দিয়ে আবার মাপুন। আপনার সংখ্যা কি ৪, ৪, ১, ১ আর ৪, ৪, ৪, ৪-এর সাথে মেলে?

    Dev server-এ React-এর StrictMode development-এ render দ্বিগুণ করে, তাই গোনার জন্য production build চালান, অথবা main.tsx থেকে StrictMode সরান। Vue-তে setup-এর ভেতরে গুনবেন না, কারণ সেটি একবারই চলে; onMounted আর onUpdated ব্যবহার করুন।

    প্রত্যাশিত ফল: Vue-তে Parent ৪, Badge ৪, Name ১, Static ১. React-এ memo ছাড়া চারটিই ৪, memo দিলে Name আর Static ১. Version আলাদা হলে bundle-এর আকার আলাদা আসবে, কিন্তু render-এর সংখ্যা এই নিয়মেই থাকা উচিত। না মিললে দেখুন Parent কি নতুন object বা function prop হিসেবে পাঠাচ্ছে, কারণ তাতে memo আর Vue-র props মেলানো দুটোই ব্যর্থ হয়।

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

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

ক্রমNuxt অ্যাপে প্রথম পাতা খোলা থেকে আরেকটি পাতায় যাওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🌐 Browser server-এর কাছে পাতার URL চায়
  2. ⚙️ Server-এ component-এর setup চলে, useFetch data আনে
  3. 📄 Data সহ HTML আর তার ভেতরে payload browser-এ আসে
  4. 💧 Browser JS নামিয়ে hydration করে, data নেয় payload থেকে
  5. 🔗 ব্যবহারকারী link চাপেন, browser নিজে API থেকে নতুন data আনে

Vue 3 কীভাবে জানে কোন component আবার render করতে হবে?

  • ডেভেলপার প্রতিটি component-এ dependency array লিখে দেন
  • Render চলার সময় Proxy লিখে রাখে কোন মান পড়া হলো, আর সেই মান বদলালে শুধু তাদের আবার চালায়
  • প্রতি ১৬ মিলিসেকেন্ডে পুরো অ্যাপ নতুন করে render করে মেলায়
  • যেকোনো মান বদলালে root থেকে সব component আবার চালায়

একটি Parent component-এর count বদলায়। তার সন্তান Static কোনো prop নেয় না আর কোনো state পড়ে না। memo ছাড়া React-এ আর Vue 3-এ Static-এর কী হবে?

  • দুটিতেই Static আবার render হবে না
  • দুটিতেই Static আবার render হবে
  • React-এ Static আবার render হবে, Vue-তে হবে না
  • Vue-তে Static আবার render হবে, React-এ হবে না

Pinia store থেকে const { count } = useCart() লেখা হলো, যেখানে count একটি computed. ঝুড়িতে পণ্য যোগ হলে template-এ count-এর কী হবে?

  • আগের মানেই আটকে থাকবে, কারণ destructure করায় reactive সংযোগ ছিঁড়ে গেছে
  • ঠিকমতো বাড়বে, Pinia সব সামলায়
  • Build-এর সময় error দেবে
  • অসীম render loop শুরু হবে

Nuxt-এর একটি পাতার <script setup>-এ API থেকে পণ্যের তালিকা আনতে হবে, আর পাতাটি server-এ render হয়। কোনটি সবচেয়ে উপযুক্ত?

  • onMounted-এর ভেতরে $fetch
  • সরাসরি await $fetch('/api/products')
  • XMLHttpRequest দিয়ে হাতে লেখা অনুরোধ
  • await useFetch('/api/products')

Vue-তে <input v-model="search"> লেখার সবচেয়ে কাছের React সমতুল্য কোনটি?

  • <input defaultValue={search} />
  • <input value={search} onChange={(e) => setSearch(e.target.value)} />
  • <input onChange={(e) => setSearch(e.target.value)} />
  • <input ref={searchRef} />

Nuxt-এর server-এ চলা কোডে একটি module-স্তরের const cart = ref([])-এ প্রতিটি ক্রেতার ঝুড়ি রাখা হলো। সবচেয়ে বড় সমস্যা কী?

  • ref server-এ একেবারেই চলে না
  • Bundle-এর আকার অনেক বেড়ে যাবে
  • একই server process-এর সব ক্রেতা একটি ঝুড়ি ভাগ করবেন, একজনের তথ্য আরেকজন দেখতে পারেন
  • শুধু একটি hydration warning আসবে, আর কিছু নয়

Vue 3-এর template compiler বনাম React-এর JSX-এর একটি প্রকৌশলগত পার্থক্য কোনটি?

  • Template সীমিত বলে compiler আগেই জানে কোন node স্থির, তাই update-এর সময় কম মেলাতে হয়
  • Template browser সরাসরি পড়ে, JSX পড়ে না
  • Template ব্যবহার করলে Vue 3.5-এ কোনো virtual DOM থাকে না
  • JSX-এ TypeScript ব্যবহার করা যায় না

শব্দকোষ

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

মনে রাখুন

  1. Vue-র component একটি SFC: script, template আর style এক ফাইলে। Template compile হয়ে render function হয়।
  2. <script setup> একবারই চলে। ref, computed, watch আর lifecycle hook দিয়ে logic সাজানো হয়।
  3. Proxy-র track আর trigger: শুধু যে effect মান পড়েছে, সে আবার চলে। React-এ একই ফল পেতে memo বা React Compiler লাগে।
  4. Props, emit, v-model, slot, Pinia আর Vue Router হলো React-এর props, callback, controlled input, children, store আর router-এর জোড়া।
  5. Nuxt: file দিয়ে route, server/api, আর useFetch-এর payload; server-এ ব্যবহারকারীর state কখনো module-এর ref-এ নয়।

পরের পর্বে: পর্ব ৬৭-এ দুটি একেবারে ভিন্ন দর্শন: Angular-এর সবকিছু-সহ কাঠামো, dependency injection আর signal, এবং Svelte-এর compiler, যা অনেক কাজ build-এর সময়েই সেরে ফেলে। বাজারঘরের একই দুই পাতা সেখানেও।

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

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