React Bangla Logo
Reactবাংলা

দেশি দোকান — Redux Toolkit + RTK Query দিয়ে সম্পূর্ণ ই-কমার্স প্রজেক্ট

হাতে-কলমে বানাবো "দেশি দোকান" — একটি সম্পূর্ণ ফাংশনাল ই-কমার্স স্টোর। থাকছে Redux Toolkit দিয়ে Cart State Management, RTK Query দিয়ে Data Fetching ও Mutation (Axios ছাড়াই), json-server দিয়ে Mock Backend, আধুনিক Tailwind CSS UI, React Router দিয়ে Multi-page navigation, আর প্রতিটা ফিচারের পর বাস্তব ডেভেলপারদের মতো Git commit/push workflow। যিনি জীবনে কখনো Redux বা Git ব্যবহার করেননি, তিনিও এই গাইড অনুসরণ করে সম্পূর্ণ প্রজেক্টটা বানাতে পারবেন।

দেশি দোকান — একটি সম্পূর্ণ হাতে-কলমে প্রজেক্ট (Redux Toolkit + RTK Query + Git)

আমাদের প্রথম প্রজেক্ট "Deshi Book"-এ আমরা Context API আর Axios Advanced দিয়ে কাজ করেছি।

Project ১: Deshi Book — Context API + Axios Advanced

এবার আমরা সম্পূর্ণ ভিন্ন একটা approach নিয়ে কাজ করব — Redux Toolkit দিয়ে global state management, আর RTK Query দিয়ে data fetching/mutation, যেখানে axios-এর কোনো দরকারই পড়বে না। আমরা বানাবো দেশি দোকান (Deshi Dokan) — একটা সম্পূর্ণ ফাংশনাল ই-কমার্স স্টোর। কভার করব চারটা ফিচার:

  1. প্রোডাক্ট ব্রাউজিং — Category filter আর Search সহ, real API থেকে fetch (RTK Query দিয়ে)।
  2. শপিং কার্ট — Add/Remove/Quantity বাড়ানো-কমানো, Redux Toolkit slice দিয়ে, localStorage-এ persist।
  3. চেকআউট — RTK Query Mutation দিয়ে অর্ডার তৈরি, backend-এ POST।
  4. অর্ডার হিস্ট্রি (বোনাস) — RTK Query-র automatic cache invalidation ম্যাজিক নিজের চোখে দেখার জন্য।

আর সবচেয়ে গুরুত্বপূর্ণ পার্থক্য — এই টিউটোরিয়ালে আমরা বাস্তব ডেভেলপারদের মতো কাজ করব: একটা ফিচার শেষ হলেই সেটা Git commit করব, একটা অর্থপূর্ণ commit message লিখব, আর নিয়মিত GitHub-এ push করব। "প্রজেক্ট শেষ করে একবারে সব কোড কমিট করা" — এটা বাস্তব ডেভেলপমেন্টে হয় না, আমরাও সেভাবে করব না।

এই প্রজেক্টের ভিত্তি একটা basic React cart/checkout রেফারেন্স প্রজেক্ট থেকে অনুপ্রাণিত (Context API + useReducer + inline style দিয়ে বানানো), কিন্তু আমরা সেটাকে সম্পূর্ণ নতুন করে, next-level আর্কিটেকচার আর আধুনিক UI দিয়ে বানাবো — কোনো কোড copy করছি না, স্ক্র্যাচ থেকে বানাচ্ছি।

ব্যবহার করব:

  • Redux ToolkitcreateSlice, configureStore দিয়ে Cart state management।
  • RTK QuerycreateApi, auto-generated hooks, caching, tag-based invalidation।
  • json-server — নিজের Mock Backend, কোনো real backend সেটআপ ছাড়াই।
  • Tailwind CSS — আধুনিক, রেসপন্সিভ UI।
  • React Router — Multi-page navigation, dynamic route params।
  • Git — ফিচার-বাই-ফিচার commit, conventional commit message, আর GitHub push।

১. প্রজেক্ট পরিচিতি — কী বানাচ্ছি, কেন এই Approach?

১.১ Reference প্রজেক্ট থেকে Next Level-এ কী কী বদলাচ্ছি

Table 1.1: আগের ধরনের basic cart প্রজেক্ট vs আমাদের "দেশি দোকান"

যেভাবে সাধারণত বানানো হয়আমরা যেভাবে বানাবো
Context API + useReducerRedux ToolkitcreateSlice + configureStore
Static hardcoded product arrayRTK Query দিয়ে real API (mock backend) থেকে fetch
Inline style / plain CSSTailwind CSS দিয়ে আধুনিক, রেসপন্সিভ UI
Cart data রিফ্রেশে হারিয়ে যায়localStorage-এ persist — রিফ্রেশ দিলেও cart থেকে যায়
শুধু Cart + Checkout+ Search, Category Filter, আর Order History (cache invalidation)
Manual useState + useEffectRTK Query-র auto-generated hooks — loading/error/caching সব built-in
প্রজেক্ট শেষে একবারে commitপ্রতিটা ফিচার শেষে আলাদা, অর্থপূর্ণ Git commit + push

১.২ কী কী শিখবেন

  • Redux Toolkit বেসিক — store, slice, action, reducer, selector — একদম শুরু থেকে।
  • RTK QuerycreateApi, fetchBaseQuery, auto-generated hooks, caching, tagTypes/providesTags/invalidatesTags
  • json-server দিয়ে নিজের mock backend বানানো — কোনো Node/Express backend লেখা ছাড়াই real HTTP API।
  • Tailwind CSS দিয়ে প্রোডাকশন-মানের UI।
  • React Router দিয়ে Multi-page navigation, dynamic route params (useParams)।
  • Git workflowinit, add, commit, status, log, .gitignore, GitHub-এ remote/push — একজন প্রফেশনাল ডেভেলপার যেভাবে দিনে কাজ করেন।

এই টিউটোরিয়ালটা সম্পূর্ণ Redux আর Git বিগিনারদের জন্য লেখা। জীবনে কখনো Redux বা Git ব্যবহার না করে থাকলেও সমস্যা নেই — প্রতিটা concept শুরু থেকে ব্যাখ্যা করা হয়েছে, কোনো prior knowledge ধরে নেওয়া হয়নি।

১.৩ Git ওয়ার্কফ্লো — বাস্তব ডেভেলপারের মতো

বাস্তব জীবনে একজন ডেভেলপার একটা প্রজেক্টে যেভাবে কাজ করেন:

  1. ছোট একটা কাজ (ফিচার, ফিক্স, বা সেটআপ ধাপ) সম্পূর্ণ করেন।
  2. git status দিয়ে দেখেন কী কী ফাইল বদলেছে।
  3. git add দিয়ে সেই পরিবর্তনগুলো stage করেন।
  4. একটা স্পষ্ট, অর্থপূর্ণ বার্তা দিয়ে git commit করেন।
  5. নিয়মিত বিরতিতে git push দিয়ে GitHub-এ পাঠান — যাতে কাজ হারিয়ে না যায় আর অন্যরা (বা ভবিষ্যতের নিজে) দেখতে পারেন।

আমরা এই টিউটোরিয়ালে ঠিক এই প্যাটার্নই অনুসরণ করব — প্রতিটা সেকশনের শেষে একটা 🔧 Git Commit বক্স থাকবে, যেখানে ঠিক কোন কমান্ডগুলো চালাতে হবে তা দেখানো থাকবে। commit message-এ আমরা Conventional Commits স্টাইল ব্যবহার করব — একটা প্রিফিক্স দিয়ে বার্তা শুরু হবে, যা দেখেই বোঝা যায় পরিবর্তনটা কী ধরনের:

Table 1.2: Conventional Commit প্রিফিক্স

প্রিফিক্সকখন ব্যবহার হয়
feat:নতুন ফিচার যোগ করলে
chore:সেটআপ, টুলিং, dependency install-এর মতো কাজে
style:শুধু স্টাইলিং/ফরম্যাটিং পরিবর্তনে (লজিক বদলায় না)
docs:ডকুমেন্টেশন (README ইত্যাদি) পরিবর্তনে
fix:কোনো বাগ ঠিক করলে

২. প্রজেক্ট Setup

২.১ Vite দিয়ে Frontend তৈরি করা

npm create vite@latest deshi-dokan -- --template react
cd deshi-dokan
npm install

২.২ Git দিয়ে Version Control শুরু করা

Git কী? — এটা একটা version control সিস্টেম যা আপনার কোডের প্রতিটা পরিবর্তনের "স্ন্যাপশট" (commit) সেভ করে রাখে। কোনো ভুল হলে আগের অবস্থায় ফিরে যাওয়া যায়, একাধিক ডেভেলপার একসাথে কাজ করতে পারেন, আর GitHub-এর মতো প্ল্যাটফর্মে push করলে কোড ব্যাকআপ থাকে এবং পোর্টফোলিওতেও দেখানো যায়।

npm create vite কমান্ডটা নিজে থেকেই একটা .gitignore ফাইল বানিয়ে দিয়েছে (node_modules, dist ইত্যাদি বাদ রাখার জন্য) — চাইলে খুলে দেখে নিন। এবার Git চালু করি:

git init
git branch -M main
git config user.name "আপনার নাম"
git config user.email "আপনার ইমেইল"

git config কমান্ডে --global ফ্ল্যাগ যোগ করলে (git config --global user.name "...") এই সেটিং কম্পিউটারের সব প্রজেক্টের জন্য একবারে সেট হয়ে যায় — প্রতিটা প্রজেক্টে আলাদা করে করতে হয় না। প্রথমবার Git ব্যবহার করলে --global দিয়েই করে নেওয়া ভালো।

এখন প্রথম commit করি:

git add .
git commit -m "chore: scaffold project with vite and react"

git log --oneline দিয়ে চেক করুন:

git log --oneline
81864ea (HEAD -> main) chore: scaffold project with vite and react

💡 এখন থেকে প্রতিটা ধাপ শেষে commit

এই টিউটোরিয়ালের বাকি অংশে যেখানেই 🔧 Git Commit বক্স দেখবেন, ঠিক সেই মুহূর্তে থেমে commit করে নেবেন — তারপর পরের সেকশনে যাবেন। এটাই বাস্তব ডেভেলপমেন্ট অভ্যাস।

২.৩ Tailwind CSS

npm install tailwindcss @tailwindcss/vite

vite.config.js আপডেট করুন:

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

src/index.css-এর সম্পূর্ণ কনটেন্ট বদলে দিন:

@import "tailwindcss";

Tailwind ঠিকমতো কাজ করছে কিনা যাচাই করতে src/App.jsx-এর কনটেন্ট সাময়িকভাবে সিম্পল করে দিন (ডিফল্ট Vite counter ডেমো, লোগো, App.css — এসব এখন আর দরকার নেই):

// src/App.jsx
function App() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <h1 className="text-3xl font-bold text-emerald-600">
        Tailwind is working! 🎉
      </h1>
    </div>
  );
}

export default App;

এখন আর দরকার নেই এমন ফাইলগুলো মুছে ফেলুন:

rm -rf src/App.css src/assets public/icons.svg

npm run dev চালিয়ে ব্রাউজারে emerald রঙের "Tailwind is working! 🎉" লেখা দেখতে পেলে সেটআপ ঠিক আছে।

🔧 Git Commit

git add .
git commit -m "style: add tailwind css"

২.৪ Redux Toolkit, react-redux, react-router ইনস্টল করা

npm install @reduxjs/toolkit react-redux react-router

🚨 react-router-dom না, শুধু react-router

বর্তমান React Router (v8, ২০২৬-এ current release) থেকে react-router-dom প্যাকেজটা আর মূলধারায় নেই — BrowserRouter, Routes, Route, Link, useNavigate, useParams সবই সরাসরি react-router থেকে import হয়। এই টিউটোরিয়ালের সব জায়গায় আমরা "react-router" থেকেই import করব।

💡 RTK Query-র জন্য আলাদা প্যাকেজ লাগে না

লক্ষ্য করুন — উপরে axios ইনস্টল করিনি। RTK Query আলাদা কোনো প্যাকেজ না — এটা @reduxjs/toolkit-এর ভেতরেই বিল্ট-ইন থাকে, @reduxjs/toolkit/query/react থেকে import হবে (সেকশন ৭-এ দেখবেন)। তাই ডাটা fetch করার জন্য axios বা fetch আলাদা করে ইনস্টল করার দরকার নেই।

২.৫ Mock Backend — json-server ইনস্টল করা

npm install -D json-server@0.17.4 concurrently

json-server কী? — এটা একটা টুল যা একটা db.json ফাইল থেকে সম্পূর্ণ REST API বানিয়ে দেয় — কোনো Node/Express কোড না লিখেই GET/POST/PUT/DELETE সহ real HTTP endpoint পাওয়া যায়। শেখার আর প্রোটোটাইপিং-এর জন্য এটা দারুণ — production backend-এর জায়গায় ব্যবহার করবেন না, কিন্তু RTK Query শেখার জন্য এটা একদম পারফেক্ট, কারণ RTK Query real HTTP request-ই পাঠায়।

⚠️ ভার্সন pin করা কেন জরুরি

json-server-এর latest ভার্সন এখনো 1.0.0-beta পর্যায়ে (npm-এ latest ট্যাগ এটাই নির্দেশ করে) — API আর CLI বদলে যেতে পারে। তাই আমরা সুনির্দিষ্টভাবে json-server@0.17.4 ব্যবহার করছি, যেটা সবচেয়ে স্টেবল আর ডকুমেন্টেড ভার্সন।

concurrently কেন লাগবে? — আমাদের দুটো সার্ভার একসাথে চালাতে হবে: Vite dev server (frontend) আর json-server (mock backend)। concurrently দিয়ে একটাই কমান্ডে দুটোই চালানো যাবে (সেকশন ২.৮-এ দেখবেন)।

২.৬ Folder Structure

src/
├── app/
│   └── store.js               ← Redux store (সব reducer এক জায়গায়)
├── features/
│   └── cart/
│       └── cartSlice.js       ← Cart state (Redux Toolkit slice)
├── services/
│   └── productsApi.js         ← RTK Query API slice (products + orders)
├── components/
│   ├── Navbar.jsx
│   ├── ProductCard.jsx
│   ├── ProductGrid.jsx
│   ├── CategoryFilter.jsx
│   ├── SearchBar.jsx
│   └── CartItem.jsx
├── pages/
│   ├── HomePage.jsx            ← প্রোডাক্ট লিস্ট + filter/search
│   ├── ProductDetailsPage.jsx  ← একটা প্রোডাক্টের ডিটেইলস
│   ├── CartPage.jsx
│   ├── CheckoutPage.jsx
│   ├── OrderSuccessPage.jsx
│   └── OrdersPage.jsx          ← Order History (বোনাস ফিচার)
├── App.jsx
└── main.jsx

server/
└── db.json                     ← json-server-এর ডাটা (products + orders)

২.৭ .env বনাম .env.example — Git থেকে Config বাদ রাখা

এখানে একটা গুরুত্বপূর্ণ real-world Git অভ্যাস শিখব। .env ফাইলে local configuration (যেমন API URL, বা প্রোডাকশনে secret key) থাকে — এটা সাধারণত Git-এ commit করা হয় না, কারণ:

  • প্রতিটা ডেভেলপারের/এনভায়রনমেন্টের নিজস্ব ভ্যালু থাকতে পারে।
  • Secret key (API key, database password) থাকলে GitHub-এ পাবলিকভাবে ফাঁস হয়ে যেতে পারে।

তার বদলে একটা .env.example ফাইল commit করা হয় — যেটা শুধু কাঠামো দেখায় (আসল secret ছাড়া), যাতে অন্য কেউ (বা ভবিষ্যতের আপনি) repo clone করে বুঝতে পারেন কোন environment variable গুলো লাগবে।

প্রজেক্টের root-এ .env.example বানান:

# .env.example
VITE_API_BASE_URL=http://localhost:4000

এখন এটা কপি করে নিজের আসল .env বানান:

cp .env.example .env

.gitignore-এ যোগ করুন যাতে .env কখনো commit না হয়:

# .gitignore-এ যোগ করুন

# Environment variables
.env

এই প্রজেক্টে .env-এ আসলে কোনো secret নেই (শুধু একটা local URL), কিন্তু অভ্যাসটা এখনই তৈরি করা ভালো — বড় প্রজেক্টে .env-এ API key, database URL-এর মতো sensitive তথ্য থাকবে, আর তখন এই অভ্যাসটাই আপনাকে বাঁচাবে।

২.৮ package.json-এ Script যোগ করা

{
  "scripts": {
    "dev": "vite",
    "server": "json-server server/db.json --port 4000",
    "dev:all": "concurrently \"npm run server\" \"npm run dev\"",
    "build": "vite build",
    "preview": "vite preview"
  }
}

এখন থেকে শুধু একটা কমান্ডেই (npm run dev:all) frontend আর mock backend দুটোই একসাথে চালু হবে — বিস্তারিত সেকশন ২৩-এ দেখবেন।

২.৯ Mock Data — server/db.json

server নামে একটা ফোল্ডার বানিয়ে ভেতরে db.json ফাইল তৈরি করুন:

// server/db.json
{
  "products": [
    {
      "id": 1,
      "name": "Noise Cancelling Wireless Headphone",
      "category": "Electronics",
      "price": 3490,
      "oldPrice": 4200,
      "image": "https://picsum.photos/seed/headphone1/500/500",
      "description": "৪০ ঘণ্টা ব্যাটারি ব্যাকআপ, অ্যাক্টিভ নয়েজ ক্যান্সেলেশন, এবং ব্লুটুথ ৫.৩ সাপোর্ট সহ প্রিমিয়াম হেডফোন।",
      "stock": 18,
      "rating": 4.5
    },
    {
      "id": 2,
      "name": "Smart Watch Pro",
      "category": "Electronics",
      "price": 5990,
      "oldPrice": 6990,
      "image": "https://picsum.photos/seed/smartwatch2/500/500",
      "description": "হার্ট রেট মনিটর, SpO2 সেন্সর, আর ৭ দিনের ব্যাটারি লাইফ সহ স্মার্টওয়াচ।",
      "stock": 25,
      "rating": 4.3
    },
    {
      "id": 3,
      "name": "Portable Bluetooth Speaker",
      "category": "Electronics",
      "price": 1890,
      "oldPrice": 2300,
      "image": "https://picsum.photos/seed/speaker3/500/500",
      "description": "ওয়াটারপ্রুফ ডিজাইন, ১২ ঘণ্টা প্লেব্যাক, শক্তিশালী বেস সহ পোর্টেবল স্পিকার।",
      "stock": 32,
      "rating": 4.1
    },
    {
      "id": 4,
      "name": "USB-C Fast Charger 65W",
      "category": "Electronics",
      "price": 990,
      "oldPrice": 1200,
      "image": "https://picsum.photos/seed/charger4/500/500",
      "description": "GaN টেকনোলজি সহ ৬৫ ওয়াট ফাস্ট চার্জার, ল্যাপটপ ও ফোন দুটোই চার্জ করতে পারে।",
      "stock": 40,
      "rating": 4.6
    },
    {
      "id": 5,
      "name": "Men's Casual Cotton Shirt",
      "category": "Fashion",
      "price": 890,
      "oldPrice": 1100,
      "image": "https://picsum.photos/seed/shirt5/500/500",
      "description": "১০০% কটন ফেব্রিক, রেগুলার ফিট, অফিস ও ক্যাজুয়াল দুই জায়গাতেই মানানসই।",
      "stock": 55,
      "rating": 4.2
    },
    {
      "id": 6,
      "name": "Women's Printed Kurti",
      "category": "Fashion",
      "price": 1290,
      "oldPrice": 1590,
      "image": "https://picsum.photos/seed/kurti6/500/500",
      "description": "হ্যান্ডপিক প্রিন্ট, সফট ফেব্রিক, সব সিজনে পরার উপযোগী কুর্তি।",
      "stock": 30,
      "rating": 4.4
    },
    {
      "id": 7,
      "name": "Leather Wallet for Men",
      "category": "Fashion",
      "price": 690,
      "oldPrice": 850,
      "image": "https://picsum.photos/seed/wallet7/500/500",
      "description": "জেনুইন লেদার, ৬টা কার্ড স্লট, স্লিম প্রোফাইল ডিজাইন।",
      "stock": 60,
      "rating": 4
    },
    {
      "id": 8,
      "name": "Running Sneakers",
      "category": "Fashion",
      "price": 2490,
      "oldPrice": 2990,
      "image": "https://picsum.photos/seed/sneaker8/500/500",
      "description": "লাইটওয়েট মেশ আপার, কুশনড সোল — দৌড়ানো ও দৈনন্দিন ব্যবহার দুটোর জন্যই আরামদায়ক।",
      "stock": 22,
      "rating": 4.5
    },
    {
      "id": 9,
      "name": "Non-Stick Cookware Set",
      "category": "Home & Kitchen",
      "price": 2190,
      "oldPrice": 2690,
      "image": "https://picsum.photos/seed/cookware9/500/500",
      "description": "৫ পিস নন-স্টিক কুকওয়্যার সেট, ইনডাকশন কম্প্যাটিবল।",
      "stock": 15,
      "rating": 4.3
    },
    {
      "id": 10,
      "name": "Electric Kettle 1.8L",
      "category": "Home & Kitchen",
      "price": 1090,
      "oldPrice": 1350,
      "image": "https://picsum.photos/seed/kettle10/500/500",
      "description": "অটো শাট-অফ ফিচার সহ স্টেইনলেস স্টিল ইলেকট্রিক কেটলি।",
      "stock": 28,
      "rating": 4.2
    },
    {
      "id": 11,
      "name": "Memory Foam Pillow (Set of 2)",
      "category": "Home & Kitchen",
      "price": 1490,
      "oldPrice": 1890,
      "image": "https://picsum.photos/seed/pillow11/500/500",
      "description": "অর্থোপেডিক মেমরি ফোম, ঘাড় ও কাঁধের ব্যথা কমাতে সাহায্য করে।",
      "stock": 34,
      "rating": 4.6
    },
    {
      "id": 12,
      "name": "LED Desk Lamp",
      "category": "Home & Kitchen",
      "price": 790,
      "oldPrice": 990,
      "image": "https://picsum.photos/seed/lamp12/500/500",
      "description": "৩ লেভেল ব্রাইটনেস, টাচ কন্ট্রোল, USB চার্জিং পোর্ট সহ ডেস্ক ল্যাম্প।",
      "stock": 45,
      "rating": 4.1
    },
    {
      "id": 13,
      "name": "The Silent Ocean — Novel",
      "category": "Books",
      "price": 350,
      "oldPrice": 420,
      "image": "https://picsum.photos/seed/book13/500/500",
      "description": "একটি জনপ্রিয় বাংলা উপন্যাস, পেপারব্যাক এডিশন, ৩২০ পৃষ্ঠা।",
      "stock": 50,
      "rating": 4.7
    },
    {
      "id": 14,
      "name": "Learn Web Development — Guide Book",
      "category": "Books",
      "price": 590,
      "oldPrice": 690,
      "image": "https://picsum.photos/seed/book14/500/500",
      "description": "শুরু থেকে অ্যাডভান্সড পর্যন্ত ওয়েব ডেভেলপমেন্ট শেখার সম্পূর্ণ গাইড।",
      "stock": 40,
      "rating": 4.8
    },
    {
      "id": 15,
      "name": "Kids Story Collection (5 Books)",
      "category": "Books",
      "price": 490,
      "oldPrice": 600,
      "image": "https://picsum.photos/seed/book15/500/500",
      "description": "শিশুদের জন্য ৫টি রঙিন ছবিসহ গল্পের বই একসাথে।",
      "stock": 36,
      "rating": 4.5
    },
    {
      "id": 16,
      "name": "Yoga Mat Anti-Slip",
      "category": "Fashion",
      "price": 890,
      "oldPrice": 1090,
      "image": "https://picsum.photos/seed/yogamat16/500/500",
      "description": "৬mm পুরু, অ্যান্টি-স্লিপ সারফেস, ক্যারি স্ট্র্যাপ সহ যোগা ম্যাট।",
      "stock": 27,
      "rating": 4.3
    }
  ],
  "orders": []
}

orders অ্যারে খালি রেখে শুরু করছি — চেকআউট করলে json-server নিজে থেকেই নতুন অর্ডার এখানে যোগ করবে, সাথে auto-generated id

২.১০ Git Commit — Tooling ও Mock Backend

এখন পর্যন্ত আমরা Redux Toolkit, react-redux, react-router, json-server, concurrently ইনস্টল করেছি, folder structure বানিয়েছি, .env/.env.example সেটআপ করেছি, npm script যোগ করেছি, আর mock data বানিয়েছি। এই পুরো "tooling setup" একটা commit হিসেবে যুক্তিসঙ্গত — বাস্তব ডেভেলপাররা dependency install আর config-এর কাজ প্রায়ই একসাথে commit করেন, কারণ এটা কোনো ফিচার না, শুধু ভিত্তি তৈরি।

git status

.env ফাইলটা তালিকায় দেখাবে না — কারণ .gitignore-এ যোগ করা হয়েছে। .env.example, package.json, server/db.json, .gitignore দেখাবে।

🔧 Git Commit

git add .
git commit -m "chore: setup redux toolkit, react-router and mock backend with json-server"

৩. Redux বেসিক — Store, Slice, Action, Reducer কী?

Redux ব্যবহার করিনি এমন কারো জন্য প্রথমে বুঝে নেওয়া দরকার — কেন এটা দরকার।

আমাদের Cart-এর data অনেক জায়গা থেকে দরকার হবে: Navbar-এ কার্টের আইটেম-সংখ্যা দেখাতে, ProductCard-এ "কার্টে যোগ করুন" বাটনে, CartPage-এ পুরো লিস্ট দেখাতে, CheckoutPage-এ অর্ডার বসাতে। এই সব কম্পোনেন্ট একে অপরের parent-child না — props দিয়ে data চালাচালি করলে (prop drilling) কোড অগোছালো হয়ে যাবে।

Redux-এর সমাধান: একটা central store — পুরো অ্যাপের একটাই global state, যেকোনো কম্পোনেন্ট থেকে সরাসরি পড়া আর পরিবর্তন করা যায়।

মূল চারটা concept:

  1. Store — পুরো অ্যাপের state যেখানে থাকে। একটাই store থাকে।
  2. Slice — state-এর একটা নির্দিষ্ট অংশ (যেমন cart), তার সাথে যুক্ত সব reducer একসাথে।
  3. Action — "কী পরিবর্তন করতে চাই" তার বর্ণনা (যেমন addToCart)। Component থেকে dispatch(addToCart(product)) কল করে action পাঠানো হয়।
  4. Reducer — action পেয়ে state আসলে কীভাবে বদলাবে সেই ফাংশন।

Flow: Component dispatch করে একটা action → সেই slice-এর reducer state আপডেট করে → যেই component useSelector দিয়ে সেই অংশটুকু subscribe করে আছে, সে automatically re-render হয়।

💡 পুরনো Redux vs Redux Toolkit

পুরনো (plain) Redux-এ প্রতিটা action-এর জন্য আলাদা action creator, switch-case দিয়ে বিশাল reducer, আর state সবসময় immutably (নতুন object বানিয়ে) আপডেট করতে হতো — অনেক boilerplate। Redux Toolkit (RTK) এই সব সরল করে দেয়: createSlice দিয়ে reducer লিখলেই action creator নিজে থেকেই তৈরি হয়, আর ভেতরে Immer লাইব্রেরি থাকার কারণে সরাসরি state.items.push(...)-এর মতো "mutate" করার মতো কোড লিখলেও আসলে immutably আপডেট হয়। এখন থেকে যখনই "Redux" বলব, বোঝাবো Redux Toolkit


৪. Cart Slice তৈরি করা

// src/features/cart/cartSlice.js
import { createSlice } from "@reduxjs/toolkit";

const CART_STORAGE_KEY = "deshi-dokan-cart";

// localStorage থেকে আগের cart data load করা (refresh দিলে যেন হারিয়ে না যায়)
const loadCartFromStorage = () => {
  try {
    const data = localStorage.getItem(CART_STORAGE_KEY);
    return data ? JSON.parse(data) : [];
  } catch {
    return [];
  }
};

const initialState = {
  // items: [{ id, name, price, image, qty }]
  items: loadCartFromStorage(),
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    addToCart: (state, action) => {
      const product = action.payload;
      const existingItem = state.items.find((item) => item.id === product.id);

      if (existingItem) {
        // আগে থেকেই cart-এ থাকলে শুধু quantity বাড়াবো
        existingItem.qty += 1;
      } else {
        // নতুন হলে qty: 1 সহ যোগ করবো
        state.items.push({
          id: product.id,
          name: product.name,
          price: product.price,
          image: product.image,
          qty: 1,
        });
      }
    },
    removeFromCart: (state, action) => {
      state.items = state.items.filter((item) => item.id !== action.payload);
    },
    increaseQty: (state, action) => {
      const item = state.items.find((item) => item.id === action.payload);
      if (item) item.qty += 1;
    },
    decreaseQty: (state, action) => {
      const item = state.items.find((item) => item.id === action.payload);
      // qty 1-এর নিচে যাবে না — 1 হলে decrease বাটন কাজ করবে না
      if (item && item.qty > 1) item.qty -= 1;
    },
    clearCart: (state) => {
      state.items = [];
    },
  },
});

export const {
  addToCart,
  removeFromCart,
  increaseQty,
  decreaseQty,
  clearCart,
} = cartSlice.actions;

export default cartSlice.reducer;

// ---- Selectors ----
// Component-এ useSelector(selectCartItems) এভাবে ব্যবহার হবে
export const selectCartItems = (state) => state.cart.items;

export const selectCartTotalQty = (state) =>
  state.cart.items.reduce((sum, item) => sum + item.qty, 0);

export const selectCartTotalPrice = (state) =>
  state.cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);

export { CART_STORAGE_KEY };

৪.১ লক্ষ্য করার মতো কিছু জিনিস

  • existingItem.qty += 1 — সরাসরি mutate করার মতো দেখতে হলেও, createSlice-এর ভেতরে Immer থাকায় এটা নিরাপদ এবং সঠিকভাবে immutable আপডেট তৈরি করে। কিন্তু এই "mutate করা"-র সুবিধা শুধু createSlice-এর reducer-এর ভেতরেই কাজ করে — অন্য কোথাও রেগুলার state সরাসরি বদলানো যাবে না।
  • Selector ফাংশনগুলো (selectCartItems, selectCartTotalQty, selectCartTotalPrice) — এগুলো সরাসরি slice-এর অংশ না, কিন্তু একই ফাইলে রাখা ভালো অভ্যাস, কারণ state-এর shape (state.cart.items) কোথায় আছে সেটা একজায়গায় জানা থাকে। State-এর গঠন বদলালে শুধু এই ফাইলটাই বদলাতে হবে, component না।
  • loadCartFromStorage — component mount হওয়ার আগেই, initialState সেট হওয়ার সময়েই একবার চলে। ফলে পেজ প্রথমবার লোড হওয়ার সময়েই আগের cart data নিয়ে শুরু হয়।

৪.২ Git Commit — Cart Slice

একটা সম্পূর্ণ, স্বনির্ভর ফিচার (cart state management) শেষ হলো — এটাই একটা commit-এর সঠিক সাইজ।

git status

শুধু src/features/cart/cartSlice.js নতুন ফাইল হিসেবে দেখাবে।

🔧 Git Commit

git add src/features
git commit -m "feat: add cart redux slice with localStorage persistence"

৫. RTK Query কী? Axios + useEffect-এর থেকে কীভাবে আলাদা?

Deshi Book প্রজেক্টে ডাটা আনার জন্য আমরা লিখেছিলাম: useState দিয়ে data/loading/error তিনটা state, useEffect-এর ভেতরে axios call, isMounted flag, try/catch/finally। প্রতিটা API call-এর জন্য এই একই প্যাটার্ন বারবার লিখতে হতো।

RTK Query এই পুরো প্যাটার্নটাকে একটা মাত্র hook কলে নিয়ে আসে:

Table 5.1: Manual Axios + useEffect vs RTK Query

বিষয়Axios + useEffect (ম্যানুয়াল)RTK Query
Loading stateনিজে useState দিয়ে ম্যানেজ করতে হয়isLoading/isFetching — hook থেকেই পাওয়া যায়
Error stateনিজে try/catch লিখতে হয়isError/error — hook থেকেই পাওয়া যায়
Cachingনেই — একই ডাটা বারবার ফেচ হয়Automatic — একই query আবার চাইলে cache থেকে দেয়
Mutation-এর পর লিস্ট আপডেটনিজে state আপডেট করতে হয়/manual re-fetchinvalidatesTags দিলে automatic re-fetch
প্রতি API-এর জন্য কোড১৫-২০ লাইন (useState + useEffect + cleanup)৩-৫ লাইনের endpoint definition, বাকিটা auto-generate

সহজভাবে বললে — RTK Query হলো "data fetching + caching লাইব্রেরি", যেটা Redux-এর উপরেই বানানো। এটা আপনার হয়ে API call করে, ফলাফল Redux store-এ cache করে রাখে, আর একটা React hook জেনারেট করে দেয় যেটা কল করলেই data, isLoading, isError — সবকিছু রেডি পাওয়া যায়।


৬. Mock Backend Endpoints — Reference

Table 6.1: আমাদের json-server mock backend-এর সব endpoint

ফিচারMethodEndpointQuery/Bodyনোট
সব প্রোডাক্টGET/products?category=Fashion, ?q=kurticategory=All দিলে filter হবে না
একটা প্রোডাক্টGET/products/:id
নতুন অর্ডার তৈরিPOST/orders{ customerName, phone, address, items, totalPrice, createdAt }json-server নিজেই auto-increment id বসিয়ে দেয়
সব অর্ডারGET/ordersOrder History পেজে ব্যবহার হবে

json-server real production backend না — এটা শুধু db.json ফাইল read/write করা একটা fake REST API, শেখার আর প্রোটোটাইপিং-এর জন্য। প্রোডাকশনে গেলে এটার জায়গায় আপনার নিজের real backend (Express/Node, বা অন্য যেকোনো) বসাবেন — RTK Query-র কোড প্রায় অপরিবর্তিতই থাকবে, শুধু baseUrl আর হয়তো field name-গুলো নিজের backend অনুযায়ী বদলাবেন।

সার্ভার চালু করে টেস্ট করে দেখুন:

npm run server

ব্রাউজারে http://localhost:4000/products খুললেই ১৬টা প্রোডাক্টের JSON দেখতে পাবেন।


৭. Products API Slice — RTK Query-র মূল অংশ

এই ফাইলটাই এই প্রজেক্টের সবচেয়ে গুরুত্বপূর্ণ অংশ। এখানে সব endpoint (Axios প্রজেক্টের "Service Layer"-এর সমতুল্য) এক জায়গায় সংগঠিত থাকবে।

// src/services/productsApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const productsApi = createApi({
  reducerPath: "productsApi",
  baseQuery: fetchBaseQuery({ baseUrl: import.meta.env.VITE_API_BASE_URL }),
  // tagTypes: cache invalidation-এর জন্য কোন কোন "গ্রুপ" ডাটা আছে সেটার নাম
  tagTypes: ["Product", "Order"],
  endpoints: (builder) => ({
    // ১. সব প্রোডাক্ট আনা — category আর search অনুযায়ী filter করার সুযোগসহ
    getProducts: builder.query({
      query: ({ category, search } = {}) => {
        const params = new URLSearchParams();
        if (category && category !== "All") {
          params.append("category", category);
        }
        if (search && search.trim()) {
          params.append("q", search.trim());
        }
        return `/products?${params.toString()}`;
      },
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: "Product", id })),
              { type: "Product", id: "LIST" },
            ]
          : [{ type: "Product", id: "LIST" }],
    }),

    // ২. একটা নির্দিষ্ট প্রোডাক্টের ডিটেইলস আনা
    getProductById: builder.query({
      query: (id) => `/products/${id}`,
      providesTags: (result, error, id) => [{ type: "Product", id }],
    }),

    // ৩. নতুন অর্ডার তৈরি করা (Checkout-এ ব্যবহার হবে)
    createOrder: builder.mutation({
      query: (orderData) => ({
        url: "/orders",
        method: "POST",
        body: orderData,
      }),
      // অর্ডার তৈরি হওয়ার পর Order লিস্ট invalidate হবে,
      // ফলে useGetOrdersQuery নিজে থেকেই re-fetch হয়ে নতুন অর্ডারসহ দেখাবে
      invalidatesTags: [{ type: "Order", id: "LIST" }],
    }),

    // ৪. সব অর্ডার আনা (Order History পেজে ব্যবহার হবে)
    getOrders: builder.query({
      query: () => "/orders",
      providesTags: [{ type: "Order", id: "LIST" }],
    }),
  }),
});

export const {
  useGetProductsQuery,
  useGetProductByIdQuery,
  useCreateOrderMutation,
  useGetOrdersQuery,
} = productsApi;

৭.১ প্রতিটা অংশ কী করছে

  • createApi — একটাই কল দিয়ে পুরো API slice তৈরি হয়, ঠিক যেমন createSlice দিয়ে state slice বানাই।
  • fetchBaseQuery — RTK Query-র নিজস্ব লাইটওয়েট fetch wrapper (axios-এর প্রয়োজন নেই), baseUrl একবার দিলে প্রতিটা endpoint-এ শুধু বাকি path লিখলেই হয়।
  • builder.query — GET-জাতীয় (ডাটা আনা) endpoint-এর জন্য।
  • builder.mutation — POST/PUT/DELETE-জাতীয় (ডাটা পরিবর্তন করা) endpoint-এর জন্য।
  • Auto-generated hooksgetProducts এন্ডপয়েন্ট থেকে automatically useGetProductsQuery হুক তৈরি হয়ে যায় (নাম প্যাটার্ন: use + PascalCase এন্ডপয়েন্ট নাম + Query/Mutation)। এই হুকগুলোই component-এ ব্যবহার করব — নিজে কিছু বানাতে হবে না।

৭.২ providesTags আর invalidatesTags — RTK Query-র আসল জাদু

এটাই RTK Query-কে axios+useEffect থেকে আলাদা করে এমন সবচেয়ে গুরুত্বপূর্ণ ফিচার। মেন্টাল মডেলটা এভাবে ভাবুন:

  1. getOrders কোয়েরি বলছে — "আমি { type: 'Order', id: 'LIST' } ট্যাগের ডাটা সাপ্লাই করছি" (providesTags)।
  2. createOrder মিউটেশন বলছে — "আমি সফল হলে { type: 'Order', id: 'LIST' } ট্যাগটা invalidate (বাতিল) করে দাও" (invalidatesTags)।
  3. যখনই createOrder সফল হয়, RTK Query নিজে থেকেই চেক করে — "এই মুহূর্তে কোন কোন active hook এই ট্যাগ subscribe করে আছে?" OrdersPage-এ যদি useGetOrdersQuery() তখন mount করা থাকে, RTK Query automatically সেই query আবার re-fetch করে ফেলে।

মানে — অর্ডার প্লেস করার পর "Order History লিস্ট রিফ্রেশ করো" বলে আলাদা কোনো কোড লিখতে হয় না। শুধু ট্যাগ মিলিয়ে দিলেই RTK Query নিজে থেকেই কাজটা করে দেয়। এই behavior আমরা সেকশন ২২-এ নিজের চোখে যাচাই করব।

getProducts-এ id: 'LIST' প্যাটার্নটা লক্ষ্য করুন — এটা একটা প্রচলিত কনভেনশন যেখানে পুরো লিস্টের জন্য একটা "catch-all" ট্যাগ রাখা হয়, পাশাপাশি প্রতিটা আইটেমের জন্য আলাদা ট্যাগও ({ type: 'Product', id }) দেওয়া হয়। এতে ভবিষ্যতে চাইলে "শুধু একটা প্রোডাক্ট আপডেট হলে শুধু সেটাই invalidate করো, পুরো লিস্ট না" — এই সূক্ষ্ম নিয়ন্ত্রণও সম্ভব হয়।

৭.৩ Git Commit — Products API Slice

🔧 Git Commit

git add src/services
git commit -m "feat: add products api slice with rtk query"

৮. Store তৈরি করা এবং App-কে Provider দিয়ে wrap করা

cartSlice (সেকশন ৪) আর productsApi (সেকশন ৭) — দুটোই এখন রেডি। এবার এদের একটা central store-এ একসাথে জোড়া লাগানোর পালা।

// src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer, { CART_STORAGE_KEY } from "../features/cart/cartSlice";
import { productsApi } from "../services/productsApi";

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    [productsApi.reducerPath]: productsApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(productsApi.middleware),
});

// প্রতিবার state পরিবর্তন হলে cart-এর সর্বশেষ অবস্থা localStorage-এ সেভ হবে,
// ফলে পেজ Reload দিলেও cart হারিয়ে যাবে না
store.subscribe(() => {
  const cartItems = store.getState().cart.items;
  localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
});

configureStore-এ দুই ধরনের reducer যোগ হয়েছে — একটা আমাদের নিজের cartReducer, আরেকটা RTK Query-র নিজস্ব productsApi.reducer (এটা RTK Query-র caching data রাখে)। middleware-এ productsApi.middleware যোগ না করলে caching, auto-refetch, polling-এর মতো RTK Query-র ফিচারগুলো কাজ করবে না — এটা must।

src/main.jsx আপডেট করুন:

// src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router";
import { store } from "./app/store.js";
import "./index.css";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </StrictMode>,
);

💡 Provider কি Context API-র মতোই?

ধারণাগতভাবে হ্যাঁ — Deshi Book প্রজেক্টের AuthContext.Provider-এর মতোই, এটাও "নিচের সব component-এ কিছু একটা available করে দেয়"। পার্থক্য হলো — AuthContext.Provider শুধু একটা নির্দিষ্ট state (auth) শেয়ার করত, কিন্তু react-redux-এর Provider পুরো Redux store-টাই সব component-এর কাছে available করে দেয়, useSelector/useDispatch hook দিয়ে যেকোনো জায়গা থেকে অ্যাক্সেস করা যায় — আলাদা করে কোনো useCart()-এর মতো custom hook বানানোর দরকার নেই।

App.jsx এখনো সেকশন ২.৩-এর সেই সিম্পল "Tailwind is working!" প্লেসহোল্ডার — সেটা অপরিবর্তিত থাকবে যতক্ষণ না আমরা সেকশন ৯-এ routing শুরু করি।

৮.১ Git Commit — Store ও Provider

🔧 Git Commit

git add src/app src/main.jsx
git commit -m "feat: configure redux store and wrap app with provider"

৯. React Router বেসিক ধারণা

পুরো অ্যাপে ৬টা পেজ থাকবে (Home, Product Details, Cart, Checkout, Order Success, Orders) — এদের মধ্যে navigate করার জন্য React Router লাগবে। মূল চারটা জিনিস চিনে রাখুন, যা আমরা এখনই ব্যবহার শুরু করব:

  • <BrowserRouter> — পুরো অ্যাপকে wrap করে, URL আর UI-এর মধ্যে সংযোগ তৈরি করে (আমরা ইতিমধ্যে সেকশন ৮-এ main.jsx-এ যোগ করে দিয়েছি)।
  • <Routes> আর <Route> — কোন URL path-এ কোন কম্পোনেন্ট দেখাবে, তার ম্যাপিং।
  • <Link><a> ট্যাগের React Router সংস্করণ, পুরো পেজ reload না করেই navigate করে।
  • useParams(), useNavigate() — dynamic URL অংশ (যেমন /product/6-এর 6) পড়া, আর কোডের ভেতর থেকে navigate করা।

💡 আমরা routing ধাপে ধাপে বানাব, একবারে না

বাস্তব প্রজেক্টে সাধারণত সব page একসাথে রেডি থাকে না। তাই আমরাও App.jsx-এ একবারে সবগুলো route যোগ না করে, প্রতিটা ফিচার শেষ হওয়ার সাথে সাথে তার route যোগ করব — Feature ১ শেষ হলে / route, Feature ২ শেষ হলে /cart route, ইত্যাদি। এটাই বাস্তবসম্মত: pages রেডি হওয়ার আগেই তাদের import করলে অ্যাপ ভেঙে যাবে (Failed to resolve import)। প্রতিটা ফিচার সেকশনের শেষে App.jsx-এর হালনাগাদ কোড আর তার commit দেখতে পাবেন।


ফিচার ১: প্রোডাক্ট ব্রাউজিং

১০. Navbar — Cart Badge সহ

// src/components/Navbar.jsx
import { Link } from "react-router";
import { useSelector } from "react-redux";
import { selectCartTotalQty } from "../features/cart/cartSlice";

export default function Navbar() {
  const cartTotalQty = useSelector(selectCartTotalQty);

  return (
    <header className="bg-white border-b sticky top-0 z-10">
      <div className="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
        <Link to="/" className="text-xl font-bold text-emerald-700">
          দেশি দোকান
        </Link>

        <nav className="flex items-center gap-6 text-sm font-medium text-gray-700">
          <Link to="/" className="hover:text-emerald-700">
            হোম
          </Link>
          <Link to="/orders" className="hover:text-emerald-700">
            আমার অর্ডার
          </Link>
          <Link to="/cart" className="relative hover:text-emerald-700">
            কার্ট
            {cartTotalQty > 0 && (
              <span className="absolute -top-2 -right-4 bg-emerald-600 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
                {cartTotalQty}
              </span>
            )}
          </Link>
        </nav>
      </div>
    </header>
  );
}

useSelector(selectCartTotalQty) — এই একটা লাইনেই Navbar কার্টের মোট আইটেম-সংখ্যায় subscribe হয়ে গেল। cartSlice-এর যেকোনো reducer কার্ট বদলালেই, এই কম্পোনেন্ট automatically re-render হয়ে নতুন সংখ্যা দেখাবে — কোনো prop pass করা লাগছে না।

১১. ProductCard কম্পোনেন্ট

// src/components/ProductCard.jsx
import { Link } from "react-router";
import { useDispatch } from "react-redux";
import { addToCart } from "../features/cart/cartSlice";

export default function ProductCard({ product }) {
  const dispatch = useDispatch();

  const handleAddToCart = (e) => {
    e.preventDefault(); // Link-এর ভেতরে বাটন, তাই navigate হয়ে যাওয়া ঠেকাতে হবে
    dispatch(addToCart(product));
  };

  const discountPercent = product.oldPrice
    ? Math.round(100 - (product.price / product.oldPrice) * 100)
    : 0;

  return (
    <Link
      to={`/product/${product.id}`}
      className="bg-white border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow block"
    >
      <div className="relative">
        <img
          src={product.image}
          alt={product.name}
          className="w-full h-44 object-cover"
        />
        {discountPercent > 0 && (
          <span className="absolute top-2 left-2 bg-emerald-600 text-white text-xs px-2 py-1 rounded">
            -{discountPercent}%
          </span>
        )}
      </div>

      <div className="p-3 space-y-1">
        <p className="text-xs text-gray-500">{product.category}</p>
        <h3 className="font-medium text-gray-800 line-clamp-2 min-h-[2.5rem]">
          {product.name}
        </h3>

        <div className="flex items-center gap-2">
          <span className="font-bold text-emerald-700">
            ৳{product.price}
          </span>
          {product.oldPrice && (
            <span className="text-xs text-gray-400 line-through">
              ৳{product.oldPrice}
            </span>
          )}
        </div>

        <button
          onClick={handleAddToCart}
          className="w-full mt-2 bg-emerald-600 text-white text-sm py-1.5 rounded hover:bg-emerald-700"
        >
          কার্টে যোগ করুন
        </button>
      </div>
    </Link>
  );
}

পুরো কার্ডটাই <Link>, আর ভেতরে "কার্টে যোগ করুন" বাটনও আছে। বাটনে ক্লিক করলে e.preventDefault() না দিলে ব্রাউজার প্রোডাক্ট ডিটেইলস পেজেও নিয়ে যাবে, আবার কার্টেও যোগ হবে — দুটো আচরণ মিলে যাবে। preventDefault() দিয়ে শুধু "কার্টে যোগ করা" অংশটাই ঘটবে, navigation হবে না।

১২. ProductGrid কম্পোনেন্ট

// src/components/ProductGrid.jsx
import ProductCard from "./ProductCard";

export default function ProductGrid({ products }) {
  if (products.length === 0) {
    return (
      <p className="text-center text-gray-500 mt-10">
        কোনো প্রোডাক্ট পাওয়া যায়নি। অন্য ক্যাটাগরি বা সার্চ চেষ্টা করুন।
      </p>
    );
  }

  return (
    <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

১৩. CategoryFilter কম্পোনেন্ট

// src/components/CategoryFilter.jsx
const CATEGORIES = ["All", "Electronics", "Fashion", "Home & Kitchen", "Books"];

export default function CategoryFilter({ selected, onSelect }) {
  return (
    <div className="flex flex-wrap gap-2">
      {CATEGORIES.map((category) => (
        <button
          key={category}
          onClick={() => onSelect(category)}
          className={`px-4 py-1.5 rounded-full text-sm border transition-colors ${
            selected === category
              ? "bg-emerald-600 text-white border-emerald-600"
              : "bg-white text-gray-700 border-gray-300 hover:border-emerald-600"
          }`}
        >
          {category}
        </button>
      ))}
    </div>
  );
}

১৪. SearchBar কম্পোনেন্ট

// src/components/SearchBar.jsx
export default function SearchBar({ value, onChange }) {
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder="প্রোডাক্ট সার্চ করুন..."
      className="w-full sm:w-72 border rounded-lg px-4 py-2 outline-none focus:border-emerald-600"
    />
  );
}

১৫. HomePage — সবকিছু জোড়া লাগানো

// src/pages/HomePage.jsx
import { useState } from "react";
import { useGetProductsQuery } from "../services/productsApi";
import ProductGrid from "../components/ProductGrid";
import CategoryFilter from "../components/CategoryFilter";
import SearchBar from "../components/SearchBar";

export default function HomePage() {
  const [category, setCategory] = useState("All");
  const [search, setSearch] = useState("");

  // category বা search বদলালে RTK Query নিজে থেকেই নতুন query args দেখে
  // re-fetch করবে — এখানে useEffect বা manual fetch লেখার দরকার নেই
  const { data: products = [], isLoading, isFetching, isError } =
    useGetProductsQuery({ category, search });

  return (
    <div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
      <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
        <CategoryFilter selected={category} onSelect={setCategory} />
        <SearchBar value={search} onChange={setSearch} />
      </div>

      {isLoading && (
        <p className="text-center text-gray-500 mt-10">
          প্রোডাক্ট লোড হচ্ছে...
        </p>
      )}

      {isError && (
        <p className="text-center text-red-600 mt-10">
          প্রোডাক্ট লোড করা যায়নি। Mock সার্ভার (json-server) চালু আছে কিনা
          দেখুন।
        </p>
      )}

      {!isLoading && !isError && (
        <>
          {/* isFetching true থাকে filter/search বদলে নতুন request চলাকালীন সময়ে,
              isLoading শুধু প্রথমবার লোডের সময় true থাকে — দুটোর পার্থক্য বুঝা জরুরি */}
          <div className={isFetching ? "opacity-60 transition-opacity" : ""}>
            <ProductGrid products={products} />
          </div>
        </>
      )}
    </div>
  );
}

১৫.১ isLoading বনাম isFetching — পার্থক্যটা গুরুত্বপূর্ণ

Table 15.1: isLoading vs isFetching

স্টেটকখন true হয়
isLoadingশুধু প্রথমবার ডাটা fetch হওয়ার সময় (কোনো cache data-ই নেই তখনো)
isFetchingপ্রথমবার fetch-এও true হয়, আবার category/search বদলে re-fetch হওয়ার সময়ও true হয়

isLoading দিয়ে প্রথম লোডে full-page loading message দেখানো হয়েছে, আর isFetching দিয়ে filter বদলানোর সময় পুরনো ডাটা আবছা (opacity কমিয়ে) দেখিয়ে রাখা হয়েছে — যাতে UI হঠাৎ খালি হয়ে না যায়। শুধু isLoading ব্যবহার করলে প্রতিবার filter বদলালেও পুরো গ্রিড হঠাৎ মুছে গিয়ে আবার loading text দেখাত — অভিজ্ঞতা খারাপ হতো।

১৫.২ App.jsx — প্রথম Route যোগ করা

এখন Feature ১-এর সব টুকরা রেডি — App.jsx-এ প্রথমবারের মতো Navbar আর HomePage-এর route যোগ করি (আগের "Tailwind is working!" প্লেসহোল্ডারের বদলে):

// src/App.jsx
import { Routes, Route } from "react-router";
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";

function App() {
  return (
    <div className="min-h-screen bg-slate-50">
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
      </Routes>
    </div>
  );
}

export default App;

npm run dev:all চালিয়ে দেখুন — হোম পেজে প্রোডাক্ট গ্রিড, category filter, আর search box সব কাজ করছে।

১৫.৩ Git Commit — প্রোডাক্ট ব্রাউজিং

এই পুরো ফিচারটা (Navbar + ৪টা ছোট কম্পোনেন্ট + HomePage + তার routing) একসাথে একটা commit — এভাবেই বাস্তব ডেভেলপাররা "ফিচার-লেভেল" commit করেন, প্রতিটা ছোট ফাইলের জন্য আলাদা commit না করে।

🔧 Git Commit

git add src/components src/pages src/App.jsx
git commit -m "feat: add product browsing - navbar, product card, grid, filters and home page"

১৬. ProductDetailsPage

// src/pages/ProductDetailsPage.jsx
import { useParams, Link } from "react-router";
import { useDispatch } from "react-redux";
import { useGetProductByIdQuery } from "../services/productsApi";
import { addToCart } from "../features/cart/cartSlice";

export default function ProductDetailsPage() {
  const { id } = useParams();
  const dispatch = useDispatch();

  const { data: product, isLoading, isError } = useGetProductByIdQuery(id);

  if (isLoading) {
    return (
      <p className="text-center text-gray-500 mt-10">লোড হচ্ছে...</p>
    );
  }

  if (isError || !product) {
    return (
      <div className="text-center mt-10">
        <p className="text-red-600">প্রোডাক্টটি পাওয়া যায়নি।</p>
        <Link to="/" className="text-emerald-700 underline">
          হোমে ফিরে যান
        </Link>
      </div>
    );
  }

  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <Link to="/" className="text-sm text-emerald-700 hover:underline">
        ← সব প্রোডাক্টে ফিরে যান
      </Link>

      <div className="grid sm:grid-cols-2 gap-8 mt-4">
        <img
          src={product.image}
          alt={product.name}
          className="w-full rounded-xl object-cover"
        />

        <div className="space-y-3">
          <p className="text-sm text-gray-500">{product.category}</p>
          <h1 className="text-2xl font-bold text-gray-800">
            {product.name}
          </h1>

          <div className="flex items-center gap-2">
            <span className="text-2xl font-bold text-emerald-700">
              ৳{product.price}
            </span>
            {product.oldPrice && (
              <span className="text-gray-400 line-through">
                ৳{product.oldPrice}
              </span>
            )}
          </div>

          <p className="text-gray-600">{product.description}</p>
          <p className="text-sm text-gray-500">
            স্টকে আছে: {product.stock} পিস · রেটিং: {product.rating} ⭐
          </p>

          <button
            onClick={() => dispatch(addToCart(product))}
            className="bg-emerald-600 text-white px-6 py-2.5 rounded-lg hover:bg-emerald-700"
          >
            কার্টে যোগ করুন
          </button>
        </div>
      </div>
    </div>
  );
}

useParams() দিয়ে URL-এর :id অংশ (যেমন /product/6) থেকে 6 বের করা হয়েছে, তারপর সরাসরি useGetProductByIdQuery(id)-তে পাঠানো হয়েছে — RTK Query নিজে থেকেই id বদলালে (অন্য প্রোডাক্টে ক্লিক করলে) নতুন করে fetch করবে।

App.jsx-এ নতুন route যোগ করুন:

// src/App.jsx
import { Routes, Route } from "react-router";
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";
import ProductDetailsPage from "./pages/ProductDetailsPage";

function App() {
  return (
    <div className="min-h-screen bg-slate-50">
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/product/:id" element={<ProductDetailsPage />} />
      </Routes>
    </div>
  );
}

export default App;

🔧 Git Commit

git add src/pages/ProductDetailsPage.jsx src/App.jsx
git commit -m "feat: add product details page"

ফিচার ২: শপিং কার্ট

১৭. CartItem কম্পোনেন্ট

// src/components/CartItem.jsx
import { useDispatch } from "react-redux";
import {
  increaseQty,
  decreaseQty,
  removeFromCart,
} from "../features/cart/cartSlice";

export default function CartItem({ item }) {
  const dispatch = useDispatch();

  return (
    <div className="flex items-center gap-4 bg-white border rounded-lg p-3">
      <img
        src={item.image}
        alt={item.name}
        className="w-20 h-20 object-cover rounded"
      />

      <div className="flex-1">
        <h3 className="font-medium text-gray-800">{item.name}</h3>
        <p className="text-emerald-700 font-bold">৳{item.price}</p>

        <div className="flex items-center gap-3 mt-2">
          <button
            onClick={() => dispatch(decreaseQty(item.id))}
            className="w-7 h-7 border rounded flex items-center justify-center hover:bg-gray-100"
          >

          </button>
          <span className="w-6 text-center">{item.qty}</span>
          <button
            onClick={() => dispatch(increaseQty(item.id))}
            className="w-7 h-7 border rounded flex items-center justify-center hover:bg-gray-100"
          >
            +
          </button>
        </div>
      </div>

      <div className="text-right">
        <p className="font-bold text-gray-800">৳{item.price * item.qty}</p>
        <button
          onClick={() => dispatch(removeFromCart(item.id))}
          className="text-red-600 text-sm mt-2 hover:underline"
        >
          মুছে ফেলুন
        </button>
      </div>
    </div>
  );
}

১৮. CartPage

// src/pages/CartPage.jsx
import { Link } from "react-router";
import { useSelector } from "react-redux";
import {
  selectCartItems,
  selectCartTotalPrice,
} from "../features/cart/cartSlice";
import CartItem from "../components/CartItem";

export default function CartPage() {
  const cartItems = useSelector(selectCartItems);
  const totalPrice = useSelector(selectCartTotalPrice);

  if (cartItems.length === 0) {
    return (
      <div className="text-center mt-16">
        <p className="text-gray-500">আপনার কার্ট এখনো খালি।</p>
        <Link to="/" className="text-emerald-700 underline">
          শপিং শুরু করুন
        </Link>
      </div>
    );
  }

  return (
    <div className="max-w-3xl mx-auto px-4 py-6 space-y-4">
      <h1 className="text-xl font-bold text-gray-800">আপনার কার্ট</h1>

      <div className="space-y-3">
        {cartItems.map((item) => (
          <CartItem key={item.id} item={item} />
        ))}
      </div>

      <div className="bg-white border rounded-lg p-4 flex items-center justify-between">
        <span className="text-gray-600">সর্বমোট</span>
        <span className="text-xl font-bold text-emerald-700">
          ৳{totalPrice}
        </span>
      </div>

      <Link
        to="/checkout"
        className="block text-center bg-emerald-600 text-white py-3 rounded-lg hover:bg-emerald-700"
      >
        চেকআউট করুন
      </Link>
    </div>
  );
}

লক্ষ্য করুন — cartItems আর totalPrice দুটোই সরাসরি cartSlice.js-এর selector থেকে আসছে। কোথাও useState, কোথাও useEffect নেই — Redux store-ই একমাত্র উৎস।

App.jsx-এ /cart route যোগ করুন:

// src/App.jsx
import { Routes, Route } from "react-router";
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";
import ProductDetailsPage from "./pages/ProductDetailsPage";
import CartPage from "./pages/CartPage";

function App() {
  return (
    <div className="min-h-screen bg-slate-50">
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/product/:id" element={<ProductDetailsPage />} />
        <Route path="/cart" element={<CartPage />} />
      </Routes>
    </div>
  );
}

export default App;

🔧 Git Commit

git add src/components/CartItem.jsx src/pages/CartPage.jsx src/App.jsx
git commit -m "feat: add shopping cart page"

ফিচার ৩: চেকআউট (RTK Query Mutation)

১৯. useCreateOrderMutation কীভাবে কাজ করে

Query hook (যেমন useGetProductsQuery) কল করলেই সাথে সাথে fetch শুরু হয়ে যায়। কিন্তু Mutation hook আলাদাভাবে কাজ করে — এটা সরাসরি request পাঠায় না, বরং একটা trigger function রিটার্ন করে, যেটা পরে (যেমন ফর্ম সাবমিটের সময়) কল করলে তখন request যায়।

const [createOrder, { isLoading, isError }] = useCreateOrderMutation();
// createOrder(orderData) কল করলে তখনই request পাঠানো হবে

এখানে array destructuring ব্যবহার হয়েছে (query hook-এ হয় object destructuring, যেমন const { data, isLoading } = useGetProductsQuery()) — এই পার্থক্যটা মনে রাখা জরুরি, ভুল করে mutation hook-কে object-এর মতো destructure করলে error হবে।

.unwrap() — mutation trigger করার সময় .unwrap() যোগ করলে, ব্যর্থ হলে সরাসরি catch ব্লকে ধরা পড়বে (await-এর সাথে ব্যবহার উপযোগী)। .unwrap() ছাড়া RTK Query error throw করে না, শুধু রিটার্ন করা result object-এর ভেতরে error রেখে দেয়।

২০. CheckoutPage সম্পূর্ণ কোড

// src/pages/CheckoutPage.jsx
import { useState } from "react";
import { useNavigate, Link } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import {
  selectCartItems,
  selectCartTotalPrice,
  clearCart,
} from "../features/cart/cartSlice";
import { useCreateOrderMutation } from "../services/productsApi";

export default function CheckoutPage() {
  const cartItems = useSelector(selectCartItems);
  const totalPrice = useSelector(selectCartTotalPrice);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const [form, setForm] = useState({ customerName: "", phone: "", address: "" });

  // useCreateOrderMutation থেকে trigger function আর { isLoading, isError } state পাওয়া যায়
  const [createOrder, { isLoading, isError }] = useCreateOrderMutation();

  const handleChange = (e) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const orderData = {
      customerName: form.customerName,
      phone: form.phone,
      address: form.address,
      items: cartItems.map((item) => ({
        productId: item.id,
        name: item.name,
        price: item.price,
        qty: item.qty,
      })),
      totalPrice,
      createdAt: new Date().toISOString(),
    };

    try {
      // .unwrap() করলে mutation ব্যর্থ হলে সরাসরি catch ব্লকে পড়ে —
      // .unwrap() ছাড়া RTK Query error throw করে না, শুধু state-এ রাখে
      const createdOrder = await createOrder(orderData).unwrap();
      dispatch(clearCart());
      navigate(`/order-success/${createdOrder.id}`);
    } catch (err) {
      console.error("অর্ডার তৈরি করা যায়নি:", err);
    }
  };

  if (cartItems.length === 0) {
    return (
      <div className="text-center mt-16">
        <p className="text-gray-500">কার্ট খালি — চেকআউট করার আগে প্রোডাক্ট যোগ করুন।</p>
        <Link to="/" className="text-emerald-700 underline">
          শপিং শুরু করুন
        </Link>
      </div>
    );
  }

  return (
    <div className="max-w-lg mx-auto px-4 py-6">
      <h1 className="text-xl font-bold text-gray-800 mb-4">চেকআউট</h1>

      <form
        onSubmit={handleSubmit}
        className="bg-white border rounded-lg p-5 space-y-3"
      >
        <input
          name="customerName"
          value={form.customerName}
          onChange={handleChange}
          placeholder="আপনার নাম"
          className="w-full border rounded p-2"
          required
        />
        <input
          name="phone"
          value={form.phone}
          onChange={handleChange}
          placeholder="মোবাইল নাম্বার"
          className="w-full border rounded p-2"
          required
        />
        <textarea
          name="address"
          value={form.address}
          onChange={handleChange}
          placeholder="সম্পূর্ণ ঠিকানা"
          rows={3}
          className="w-full border rounded p-2 resize-none"
          required
        />

        <div className="flex items-center justify-between border-t pt-3">
          <span className="text-gray-600">সর্বমোট</span>
          <span className="text-lg font-bold text-emerald-700">
            ৳{totalPrice}
          </span>
        </div>

        {isError && (
          <p className="text-red-600 text-sm">
            অর্ডার তৈরি করা যায়নি। আবার চেষ্টা করুন।
          </p>
        )}

        <button
          type="submit"
          disabled={isLoading}
          className="w-full bg-emerald-600 text-white py-2.5 rounded-lg hover:bg-emerald-700 disabled:opacity-60"
        >
          {isLoading ? "অর্ডার হচ্ছে..." : "অর্ডার কনফার্ম করুন"}
        </button>
      </form>
    </div>
  );
}

অর্ডার সফল হওয়ার পর দুইটা কাজ হচ্ছে: dispatch(clearCart()) দিয়ে Redux cart খালি করা হচ্ছে (যেটা store.subscribe-এর মাধ্যমে localStorage-ও খালি করে দেবে), আর navigate() দিয়ে অর্ডার আইডিসহ success পেজে পাঠানো হচ্ছে।

২১. OrderSuccessPage

// src/pages/OrderSuccessPage.jsx
import { useParams, Link } from "react-router";

export default function OrderSuccessPage() {
  const { orderId } = useParams();

  return (
    <div className="max-w-md mx-auto px-4 py-16 text-center space-y-4">
      <div className="text-5xl">🎉</div>
      <h1 className="text-2xl font-bold text-gray-800">
        অর্ডার সফলভাবে সম্পন্ন হয়েছে!
      </h1>
      <p className="text-gray-600">
        আপনার অর্ডার আইডি: <span className="font-bold">#{orderId}</span>
      </p>

      <div className="flex justify-center gap-4 pt-2">
        <Link
          to="/orders"
          className="bg-emerald-600 text-white px-5 py-2 rounded-lg hover:bg-emerald-700"
        >
          আমার অর্ডার দেখুন
        </Link>
        <Link
          to="/"
          className="border border-emerald-600 text-emerald-700 px-5 py-2 rounded-lg hover:bg-emerald-50"
        >
          আরো শপিং করুন
        </Link>
      </div>
    </div>
  );
}

App.jsx-এ /checkout আর /order-success/:orderId route যোগ করুন:

// src/App.jsx
import { Routes, Route } from "react-router";
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";
import ProductDetailsPage from "./pages/ProductDetailsPage";
import CartPage from "./pages/CartPage";
import CheckoutPage from "./pages/CheckoutPage";
import OrderSuccessPage from "./pages/OrderSuccessPage";

function App() {
  return (
    <div className="min-h-screen bg-slate-50">
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/product/:id" element={<ProductDetailsPage />} />
        <Route path="/cart" element={<CartPage />} />
        <Route path="/checkout" element={<CheckoutPage />} />
        <Route path="/order-success/:orderId" element={<OrderSuccessPage />} />
      </Routes>
    </div>
  );
}

export default App;

🔧 Git Commit

git add src/pages/CheckoutPage.jsx src/pages/OrderSuccessPage.jsx src/App.jsx
git commit -m "feat: add checkout flow with rtk query mutation"

ফিচার ৪ (বোনাস): অর্ডার হিস্ট্রি — Cache Invalidation নিজে চোখে দেখা

২২. OrdersPage

// src/pages/OrdersPage.jsx
import { useGetOrdersQuery } from "../services/productsApi";

export default function OrdersPage() {
  const { data: orders = [], isLoading, isError } = useGetOrdersQuery();

  if (isLoading) {
    return <p className="text-center text-gray-500 mt-10">লোড হচ্ছে...</p>;
  }

  if (isError) {
    return (
      <p className="text-center text-red-600 mt-10">
        অর্ডার লোড করা যায়নি।
      </p>
    );
  }

  if (orders.length === 0) {
    return (
      <p className="text-center text-gray-500 mt-10">
        এখনো কোনো অর্ডার করা হয়নি।
      </p>
    );
  }

  return (
    <div className="max-w-3xl mx-auto px-4 py-6 space-y-4">
      <h1 className="text-xl font-bold text-gray-800">আমার অর্ডারসমূহ</h1>

      {/* নতুন সবার আগে দেখানোর জন্য id অনুযায়ী উল্টো সাজানো */}
      {[...orders]
        .sort((a, b) => b.id - a.id)
        .map((order) => (
          <div key={order.id} className="bg-white border rounded-lg p-4">
            <div className="flex items-center justify-between">
              <span className="font-medium">অর্ডার #{order.id}</span>
              <span className="font-bold text-emerald-700">
                ৳{order.totalPrice}
              </span>
            </div>
            <p className="text-sm text-gray-500 mt-1">
              {order.customerName} · {order.phone}
            </p>
            <p className="text-sm text-gray-500">{order.address}</p>
            <ul className="text-sm text-gray-600 mt-2 list-disc list-inside">
              {order.items.map((item) => (
                <li key={item.productId}>
                  {item.name} × {item.qty}
                </li>
              ))}
            </ul>
          </div>
        ))}
    </div>
  );
}

২২.১ যা ঘটছে পর্দার আড়ালে

  1. আপনি /orders পেজে যান — useGetOrdersQuery() চলে, { type: 'Order', id: 'LIST' } ট্যাগে subscribe হয়ে যায়, অর্ডার লিস্ট দেখায়।
  2. অন্য ট্যাবে বা পরে /checkout-এ গিয়ে একটা অর্ডার প্লেস করেন — createOrder মিউটেশন সফল হয়, invalidatesTags: [{ type: 'Order', id: 'LIST' }] কার্যকর হয়।
  3. RTK Query নিজে থেকেই চেক করে দেখে — এই ট্যাগ subscribe করা কোনো active query আছে কিনা। OrdersPage তখনও mount করা থাকলে, সেই কম্পোনেন্ট কোনো কোড ছাড়াই নিজে থেকে re-fetch হয়ে নতুন অর্ডারসহ আপডেট হয়ে যায়।

এটাই RTK Query-র সেই ফিচার যা axios+useEffect দিয়ে ম্যানুয়ালি করতে গেলে আলাদা event system বা global state manager লাগত।

App.jsx-এ শেষ route, /orders, যোগ করুন — এখন থেকে এটাই আমাদের সম্পূর্ণ, চূড়ান্ত App.jsx:

// src/App.jsx
import { Routes, Route } from "react-router";
import Navbar from "./components/Navbar";
import HomePage from "./pages/HomePage";
import ProductDetailsPage from "./pages/ProductDetailsPage";
import CartPage from "./pages/CartPage";
import CheckoutPage from "./pages/CheckoutPage";
import OrderSuccessPage from "./pages/OrderSuccessPage";
import OrdersPage from "./pages/OrdersPage";

function App() {
  return (
    <div className="min-h-screen bg-slate-50">
      <Navbar />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/product/:id" element={<ProductDetailsPage />} />
        <Route path="/cart" element={<CartPage />} />
        <Route path="/checkout" element={<CheckoutPage />} />
        <Route path="/order-success/:orderId" element={<OrderSuccessPage />} />
        <Route path="/orders" element={<OrdersPage />} />
      </Routes>
    </div>
  );
}

export default App;

🔧 Git Commit

git add src/pages/OrdersPage.jsx src/App.jsx
git commit -m "feat: add order history page with cache invalidation demo"

২৩. সবকিছু একসাথে চালানো ও টেস্ট করা

npm run dev:all

এই কমান্ড concurrently দিয়ে frontend (Vite, http://localhost:5173) আর mock backend (json-server, http://localhost:4000) — দুটোই একসাথে চালু করে দেবে।

http://localhost:5173 খুলে এই checklist ধরে টেস্ট করুন:

  1. হোম পেজে ১৬টা প্রোডাক্ট গ্রিড আকারে দেখাচ্ছে কিনা দেখুন।
  2. Category বাটনে ক্লিক করে (যেমন "Fashion") filter কাজ করছে কিনা, আর সার্চ বক্সে টাইপ করে (যেমন "kurti") search কাজ করছে কিনা যাচাই করুন।
  3. একটা প্রোডাক্টে ক্লিক করে ডিটেইলস পেজে যান, "কার্টে যোগ করুন" চাপুন। Navbar-এর কার্ট আইকনে সংখ্যা বদলাচ্ছে কিনা দেখুন।
  4. /cart-এ গিয়ে quantity বাড়ানো-কমানো, আইটেম মুছে ফেলা টেস্ট করুন।
  5. পেজ Reload দিয়ে দেখুন — cart data এখনও আছে কিনা (localStorage persistence)।
  6. "চেকআউট করুন" চেপে ফর্ম পূরণ করে "অর্ডার কনফার্ম করুন" চাপুন — Success পেজে অর্ডার আইডি দেখাচ্ছে কিনা, আর কার্ট খালি হয়ে গেছে কিনা যাচাই করুন।
  7. "আমার অর্ডার দেখুন"-এ ক্লিক করে সদ্য করা অর্ডারটা লিস্টে দেখাচ্ছে কিনা দেখুন — এটাই cache invalidation-এর প্রমাণ।

git log --oneline চালিয়ে দেখুন — এতক্ষণে ১১টা commit থাকার কথা, প্রতিটা একটা নির্দিষ্ট, সম্পূর্ণ কাজের প্রতিনিধিত্ব করছে।


২৪. বোনাস: Cart Persist করা — store.subscribe প্যাটার্ন গভীরে

সেকশন ৮-এ আমরা লিখেছিলাম:

store.subscribe(() => {
  const cartItems = store.getState().cart.items;
  localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
});

store.subscribe(callback) — Redux store-এ যেকোনো action dispatch হওয়ার পর এই callback চলে (শুধু cart-সংক্রান্ত না, productsApi-র internal action-গুলোতেও চলবে)। প্রতিবার চললে আমরা store.getState() দিয়ে বর্তমান cart items বের করে localStorage-এ সেভ করি।

এই সাধারণ প্যাটার্নটা ছোট/মাঝারি প্রজেক্টে যথেষ্ট। অনেক বড় অ্যাপে (একাধিক slice persist করতে হলে, বা selective/nested persistence দরকার হলে) কমিউনিটির জনপ্রিয় লাইব্রেরি redux-persist ব্যবহার করা হয় — এটা আরও ফিচার দেয় (migration, storage engine বদলানো, blacklist/whitelist ইত্যাদি), কিন্তু আমাদের এই এক-slice persistence-এর জন্য ম্যানুয়াল store.subscribe যথেষ্ট এবং বোঝা সহজ।


২৫. আপনার জন্য একটা কাজ

লক্ষ্য করুন, বর্তমানে Wishlist নামের কোনো ফিচার নেই — একজন ইউজার চাইলে পরে কেনার জন্য প্রোডাক্ট আলাদা করে রাখতে পারছেন না।

নির্দেশনা:

  1. src/features/wishlist/wishlistSlice.js নামে ঠিক cartSlice.js-এর মতো ধাঁচে একটা নতুন slice বানান — toggleWishlist (আছে থাকলে বাদ, না থাকলে যোগ), আর selectWishlistItems selector সহ।

  2. store.js-এ নতুন slice-টা configureStore-এর reducer object-এ যোগ করুন।

  3. ProductCard.jsx-এ একটা হার্ট (♥) আইকন বাটন যোগ করুন যেটা dispatch(toggleWishlist(product)) কল করবে — ঠিক "কার্টে যোগ করুন" বাটনের মতোই e.preventDefault() ব্যবহার করতে ভুলবেন না।

  4. WishlistPage.jsx নামে নতুন একটা পেজ বানান (অনেকটা CartPage.jsx-এর মতো) যেটা selectWishlistItems থেকে আইটেম দেখাবে, আর App.jsx-এ /wishlist route যোগ করুন।

  5. টেস্ট করুন — একটা প্রোডাক্ট wishlist-এ যোগ করুন, পেজ reload দিন (চাইলে wishlist-ও localStorage-এ persist করতে পারেন, cartSlice-এর প্যাটার্ন অনুসরণ করে), /wishlist-এ গিয়ে দেখাচ্ছে কিনা যাচাই করুন।

  6. সবশেষে, ঠিক আমাদের বাকি ফিচারগুলোর মতোই এটাও commit করুন:

    git add src/features/wishlist src/pages/WishlistPage.jsx src/components/ProductCard.jsx src/app/store.js src/App.jsx
    git commit -m "feat: add wishlist feature"
    git push

এই exercise আপনাকে independently আরেকটা সম্পূর্ণ Redux Toolkit slice বানানোর অনুশীলন দেবে — ঠিক Deshi Book প্রজেক্টের "own feed update" exercise-এর মতোই, কিন্তু এবার axios/reducer-এর বদলে Redux Toolkit দিয়ে, আর নিজের হাতে একটা real commit + push করার অনুশীলনও হয়ে যাবে।


২৬. সেরা অনুশীলন — Best Practices

Redux Architecture:

  • ✅ একটাই central store — একাধিক store বানাবেন না।
  • ✅ প্রতিটা ফিচারের state আলাদা slice-এ রাখুন (cart, ভবিষ্যতে wishlist) — একটা বিশাল slice-এ সব গুঁজে দেবেন না।
  • ✅ Selector ফাংশন slice ফাইলেই রাখুন, component-এ সরাসরি state.cart.items লিখবেন না — state-এর গঠন বদলালে এক জায়গায় বদলালেই হবে।
  • createSlice-এর বাইরে কখনো state সরাসরি mutate করবেন না — Immer-এর সুবিধা শুধু reducer-এর ভেতরেই কাজ করে।

RTK Query:

  • ✅ সব endpoint এক createApi slice-এ সংগঠিত রাখুন (আমাদের productsApi) — এটাই Axios প্রজেক্টের "Service Layer"-এর সমতুল্য।
  • ✅ Mutation-এর পর সংশ্লিষ্ট লিস্ট রিফ্রেশ করতে ম্যানুয়াল re-fetch না লিখে invalidatesTags/providesTags ব্যবহার করুন।
  • ✅ প্রথমবার লোড (isLoading) আর re-fetch (isFetching) — UI-তে দুটো আলাদাভাবে হ্যান্ডল করুন।
  • ✅ Mutation-এর ফলাফল নিয়ে সাথে সাথে কিছু করতে হলে (যেমন navigate) .unwrap() ব্যবহার করুন।

UI/Tailwind:

  • ✅ একই রঙের প্যালেট (এখানে emerald) পুরো অ্যাপ জুড়ে consistent রাখুন — ব্র্যান্ডিং একরকম থাকে।
  • ✅ Loading/Error/Empty — এই তিন অবস্থাই প্রতিটা ডাটা-নির্ভর component-এ আলাদাভাবে হ্যান্ডল করুন, খালি সাদা স্ক্রিন কখনো দেখাবেন না।

Real-world ডাটা হ্যান্ডলিং:

  • oldPrice না থাকলে discount ব্যাজ দেখাবেন না (ProductCard-এ discountPercent > 0 চেক)।
  • ✅ Array-জাতীয় ডাটার ডিফল্ট মান খালি অ্যারে দিন (data: products = []) — undefined.map()-এর মতো error এড়াতে।

Performance:

  • ✅ RTK Query-র caching-এর সুবিধা নিন — একই আর্গুমেন্টে বারবার query করলে network call আবার হবে না, cache থেকে সার্ভ হবে।
  • ✅ Filter/Search state URL-এ না রেখে (এই প্রজেক্টে) simple রাখা হয়েছে; বড় প্রজেক্টে চাইলে useSearchParams দিয়ে URL-সিঙ্ক করা যায় যাতে filter করা অবস্থায় লিংক শেয়ার করা যায়।

Git Workflow:

  • ✅ একটা commit-এ একটাই যুক্তিসঙ্গত পরিবর্তন রাখুন — একসাথে ৫টা ভিন্ন ফিচার একটা commit-এ গুঁজে দেবেন না।
  • ✅ Conventional Commits প্রিফিক্স (feat:, fix:, chore:, docs:, style:) ব্যবহার করুন — git log --oneline দেখেই ইতিহাস বোঝা যায়।
  • .env, node_modules, dist — এগুলো কখনো commit করবেন না; .gitignore দিয়ে বাদ রাখুন।
  • ✅ Commit করার আগে git status আর git diff দিয়ে দেখে নিন ঠিক কী কী পরিবর্তন হয়েছে।
  • ✅ নিয়মিত git push করুন — শুধু কাজ শেষে না, ছোট ছোট মাইলফলকেই।

২৭. প্রোডাকশনে যাওয়ার আগে যা মাথায় রাখবেন

Table 27.1: Mock backend vs Real backend

বিষয়এই টিউটোরিয়ালে (json-server)প্রোডাকশনে যা করবেন
Backenddb.json ফাইলে read/writeReal backend (Express/Node + database) — আপনার Farhan MCQ স্ট্যাকের মতোই
AuthenticationনেইLogin/JWT দরকার হলে যোগ করুন (Deshi Book প্রজেক্টের প্যাটার্ন অনুসরণযোগ্য)
Order data storageফাইলে, সার্ভার রিস্টার্টে টিকে থাকে নাDatabase (PostgreSQL/Prisma ইত্যাদি) — persist ও query-able
Cart storageBrowser-এর localStorageছোট/মাঝারি অ্যাপে localStorage যথেষ্ট; বড় অ্যাপে (লগইন করা ইউজারের জন্য cross-device sync দরকার হলে) backend-এ cart সেভ করা বিবেচনা করুন
Environment variable.env-এ VITE_API_BASE_URLপ্রতিটা environment (dev/staging/prod)-এর জন্য আলাদা env variable, Vercel/Render-এর dashboard-এ সেট করুন — .env কখনো commit করবেন না
Deploymentম্যানুয়ালি npm run buildGitHub repo-কে Vercel/Render-এর সাথে যুক্ত করলে প্রতিটা git push-এই auto-deploy হয়ে যাবে

২৮. GitHub-এ পুশ করা — Git History রিভিউ

লোকাল রিপোজিটরি ব্যাকআপ রাখতে আর পোর্টফোলিওতে দেখানোর জন্য GitHub-এ push করুন।

  1. github.com/new-এ গিয়ে একটা নতুন, খালি repository বানান (README/.gitignore না যোগ করে — আমাদের এগুলো আগে থেকেই আছে)।
  2. লোকাল repo-র সাথে remote যুক্ত করুন:
git remote add origin https://github.com/<আপনার-ইউজারনে>/deshi-dokan.git
git push -u origin main
  1. এরপর থেকে প্রতিটা নতুন commit-এর পর শুধু:
git push

এখন git log --oneline চালালে দেখা উচিত (উপর থেকে নিচে, সবচেয়ে নতুন কমিট সবার উপরে):

docs: add project readme
feat: add order history page with cache invalidation demo
feat: add checkout flow with rtk query mutation
feat: add shopping cart page
feat: add product details page
feat: add product browsing - navbar, product card, grid, filters and home page
feat: configure redux store and wrap app with provider
feat: add products api slice with rtk query
feat: add cart redux slice with localStorage persistence
chore: setup redux toolkit, react-router and mock backend with json-server
style: add tailwind css
chore: scaffold project with vite and react

১২টা commit — প্রতিটাই একটা নির্দিষ্ট, স্বনির্ভর পরিবর্তনের প্রতিনিধিত্ব করছে। GitHub-এ repo-র "Commits" ট্যাবে গেলে ঠিক এই history-টাই দেখতে পাবেন, প্রতিটা commit ক্লিক করলে তার নির্দিষ্ট diff-ও দেখা যাবে।

২৮.১ Git কমান্ড চিটশিট

Table 28.1: এই টিউটোরিয়ালে ব্যবহৃত Git কমান্ডগুলো

কমান্ডকী করে
git initনতুন Git repository শুরু করে
git statusকোন ফাইল বদলেছে/staged আছে দেখায়
git add <file/folder>নির্দিষ্ট ফাইল/ফোল্ডার commit-এর জন্য stage করে
git add .বর্তমান ফোল্ডারের সব পরিবর্তন stage করে
git commit -m "message"staged পরিবর্তনগুলোর একটা snapshot (commit) তৈরি করে
git log --onelineসংক্ষিপ্ত commit history দেখায়
git diffstaged না-করা পরিবর্তনগুলো লাইন-বাই-লাইন দেখায়
git branch -M mainবর্তমান branch-এর নাম main করে দেয়
git remote add origin <url>GitHub repo-র সাথে লোকাল repo যুক্ত করে
git push -u origin mainপ্রথমবার push করে, ভবিষ্যতের push-এর জন্য default সেট করে
git pushনতুন commit-গুলো GitHub-এ পাঠায়
git pullGitHub থেকে নতুন commit লোকালে নিয়ে আসে

উপসংহার

এই গাইডে আপনি দেশি দোকান বানিয়ে শিখলেন:

  • Setup: Vite + Tailwind + react-router (v8) + Redux Toolkit + json-server দিয়ে mock backend।
  • Redux Toolkit বেসিক: Store, Slice, Action, Reducer, Selector — cartSlice-এর মাধ্যমে হাতে-কলমে।
  • RTK Query: createApi, fetchBaseQuery, auto-generated hooks (useGetProductsQuery, useGetProductByIdQuery, useCreateOrderMutation, useGetOrdersQuery), আর সবচেয়ে গুরুত্বপূর্ণ — tagTypes/providesTags/invalidatesTags দিয়ে automatic cache invalidation।
  • প্রোডাক্ট ব্রাউজিং (ফিচার ১): Category filter, Search, Product Details — সব RTK Query দিয়ে fetch।
  • শপিং কার্ট (ফিচার ২): Add/Remove/Quantity আপডেট, localStorage persistence via store.subscribe
  • চেকআউট (ফিচার ৩): RTK Query Mutation, .unwrap(), mutation hook-এর array destructuring প্যাটার্ন।
  • অর্ডার হিস্ট্রি (ফিচার ৪, বোনাস): Cache invalidation-এর "ম্যাজিক" নিজের চোখে দেখা — অর্ডার প্লেস করলেই লিস্ট automatically আপডেট হওয়া, কোনো manual re-fetch কোড ছাড়াই।
  • React Router: Dynamic route params (useParams), useNavigate, Multi-page navigation, ধাপে ধাপে route যোগ করা।
  • Git Workflow: init, ফিচার-বাই-ফিচার add/commit (Conventional Commits), .gitignore/.env.example দিয়ে secret বাদ রাখা, GitHub-এ remote/push — একজন প্রফেশনাল ডেভেলপারের দৈনন্দিন অভ্যাস।

এই pattern গুলো — Redux Toolkit slice + RTK Query API slice + tag-based invalidation + ফিচার-বাই-ফিচার Git commit — যেকোনো production React বা Next.js প্রজেক্টে সরাসরি ব্যবহার করা যাবে। শুধু baseUrl আর endpoint-গুলো নিজের real backend অনুযায়ী বদলে নিলেই হবে।


On this page

দেশি দোকান — একটি সম্পূর্ণ হাতে-কলমে প্রজেক্ট (Redux Toolkit + RTK Query + Git)১. প্রজেক্ট পরিচিতি — কী বানাচ্ছি, কেন এই Approach?১.১ Reference প্রজেক্ট থেকে Next Level-এ কী কী বদলাচ্ছি১.২ কী কী শিখবেন১.৩ Git ওয়ার্কফ্লো — বাস্তব ডেভেলপারের মতো২. প্রজেক্ট Setup২.১ Vite দিয়ে Frontend তৈরি করা২.২ Git দিয়ে Version Control শুরু করা💡 এখন থেকে প্রতিটা ধাপ শেষে commit২.৩ Tailwind CSS২.৪ Redux Toolkit, react-redux, react-router ইনস্টল করা🚨 react-router-dom না, শুধু react-router💡 RTK Query-র জন্য আলাদা প্যাকেজ লাগে না২.৫ Mock Backend — json-server ইনস্টল করা⚠️ ভার্সন pin করা কেন জরুরি২.৬ Folder Structure২.৭ .env বনাম .env.example — Git থেকে Config বাদ রাখা২.৮ package.json-এ Script যোগ করা২.৯ Mock Data — server/db.json২.১০ Git Commit — Tooling ও Mock Backend৩. Redux বেসিক — Store, Slice, Action, Reducer কী?💡 পুরনো Redux vs Redux Toolkit৪. Cart Slice তৈরি করা৪.১ লক্ষ্য করার মতো কিছু জিনিস৪.২ Git Commit — Cart Slice৫. RTK Query কী? Axios + useEffect-এর থেকে কীভাবে আলাদা?৬. Mock Backend Endpoints — Reference৭. Products API Slice — RTK Query-র মূল অংশ৭.১ প্রতিটা অংশ কী করছে৭.২ providesTags আর invalidatesTags — RTK Query-র আসল জাদু৭.৩ Git Commit — Products API Slice৮. Store তৈরি করা এবং App-কে Provider দিয়ে wrap করা💡 Provider কি Context API-র মতোই?৮.১ Git Commit — Store ও Provider৯. React Router বেসিক ধারণা💡 আমরা routing ধাপে ধাপে বানাব, একবারে নাফিচার ১: প্রোডাক্ট ব্রাউজিং১০. Navbar — Cart Badge সহ১১. ProductCard কম্পোনেন্ট১২. ProductGrid কম্পোনেন্ট১৩. CategoryFilter কম্পোনেন্ট১৪. SearchBar কম্পোনেন্ট১৫. HomePage — সবকিছু জোড়া লাগানো১৫.১ isLoading বনাম isFetching — পার্থক্যটা গুরুত্বপূর্ণ১৫.২ App.jsx — প্রথম Route যোগ করা১৫.৩ Git Commit — প্রোডাক্ট ব্রাউজিং১৬. ProductDetailsPageফিচার ২: শপিং কার্ট১৭. CartItem কম্পোনেন্ট১৮. CartPageফিচার ৩: চেকআউট (RTK Query Mutation)১৯. useCreateOrderMutation কীভাবে কাজ করে২০. CheckoutPage সম্পূর্ণ কোড২১. OrderSuccessPageফিচার ৪ (বোনাস): অর্ডার হিস্ট্রি — Cache Invalidation নিজে চোখে দেখা২২. OrdersPage২২.১ যা ঘটছে পর্দার আড়ালে২৩. সবকিছু একসাথে চালানো ও টেস্ট করা২৪. বোনাস: Cart Persist করা — store.subscribe প্যাটার্ন গভীরে২৫. আপনার জন্য একটা কাজ২৬. সেরা অনুশীলন — Best Practices২৭. প্রোডাকশনে যাওয়ার আগে যা মাথায় রাখবেন২৮. GitHub-এ পুশ করা — Git History রিভিউ২৮.১ Git কমান্ড চিটশিটউপসংহার