দেশি দোকান — 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 ব্যবহার করেননি, তিনিও এই গাইড অনুসরণ করে সম্পূর্ণ প্রজেক্টটা বানাতে পারবেন।
এবার আমরা সম্পূর্ণ ভিন্ন একটা approach নিয়ে কাজ করব — Redux Toolkit দিয়ে global state management, আর RTK Query দিয়ে data fetching/mutation, যেখানে axios-এর কোনো দরকারই পড়বে না। আমরা বানাবো দেশি দোকান (Deshi Dokan) — একটা সম্পূর্ণ ফাংশনাল ই-কমার্স স্টোর। কভার করব চারটা ফিচার:
প্রোডাক্ট ব্রাউজিং — Category filter আর Search সহ, real API থেকে fetch (RTK Query দিয়ে)।
চেকআউট — RTK Query Mutation দিয়ে অর্ডার তৈরি, backend-এ POST।
অর্ডার হিস্ট্রি (বোনাস) — 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 Toolkit — createSlice, configureStore দিয়ে Cart state management।
json-server দিয়ে নিজের mock backend বানানো — কোনো Node/Express backend লেখা ছাড়াই real HTTP API।
Tailwind CSS দিয়ে প্রোডাকশন-মানের UI।
React Router দিয়ে Multi-page navigation, dynamic route params (useParams)।
Git workflow — init, add, commit, status, log, .gitignore, GitHub-এ remote/push — একজন প্রফেশনাল ডেভেলপার যেভাবে দিনে কাজ করেন।
এই টিউটোরিয়ালটা সম্পূর্ণ Redux আর Git বিগিনারদের জন্য লেখা। জীবনে কখনো Redux বা Git ব্যবহার না করে থাকলেও সমস্যা নেই — প্রতিটা concept শুরু থেকে ব্যাখ্যা করা হয়েছে, কোনো prior knowledge ধরে নেওয়া হয়নি।
বাস্তব জীবনে একজন ডেভেলপার একটা প্রজেক্টে যেভাবে কাজ করেন:
ছোট একটা কাজ (ফিচার, ফিক্স, বা সেটআপ ধাপ) সম্পূর্ণ করেন।
git status দিয়ে দেখেন কী কী ফাইল বদলেছে।
git add দিয়ে সেই পরিবর্তনগুলো stage করেন।
একটা স্পষ্ট, অর্থপূর্ণ বার্তা দিয়ে git commit করেন।
নিয়মিত বিরতিতে git push দিয়ে GitHub-এ পাঠান — যাতে কাজ হারিয়ে না যায় আর অন্যরা (বা ভবিষ্যতের নিজে) দেখতে পারেন।
আমরা এই টিউটোরিয়ালে ঠিক এই প্যাটার্নই অনুসরণ করব — প্রতিটা সেকশনের শেষে একটা 🔧 Git Commit বক্স থাকবে, যেখানে ঠিক কোন কমান্ডগুলো চালাতে হবে তা দেখানো থাকবে। commit message-এ আমরা Conventional Commits স্টাইল ব্যবহার করব — একটা প্রিফিক্স দিয়ে বার্তা শুরু হবে, যা দেখেই বোঝা যায় পরিবর্তনটা কী ধরনের:
Table 1.2: Conventional Commit প্রিফিক্স
প্রিফিক্স
কখন ব্যবহার হয়
feat:
নতুন ফিচার যোগ করলে
chore:
সেটআপ, টুলিং, dependency install-এর মতো কাজে
style:
শুধু স্টাইলিং/ফরম্যাটিং পরিবর্তনে (লজিক বদলায় না)
Git কী? — এটা একটা version control সিস্টেম যা আপনার কোডের প্রতিটা পরিবর্তনের "স্ন্যাপশট" (commit) সেভ করে রাখে। কোনো ভুল হলে আগের অবস্থায় ফিরে যাওয়া যায়, একাধিক ডেভেলপার একসাথে কাজ করতে পারেন, আর GitHub-এর মতো প্ল্যাটফর্মে push করলে কোড ব্যাকআপ থাকে এবং পোর্টফোলিওতেও দেখানো যায়।
npm create vite কমান্ডটা নিজে থেকেই একটা .gitignore ফাইল বানিয়ে দিয়েছে (node_modules, dist ইত্যাদি বাদ রাখার জন্য) — চাইলে খুলে দেখে নিন। এবার Git চালু করি:
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
এই টিউটোরিয়ালের বাকি অংশে যেখানেই 🔧 Git Commit বক্স দেখবেন, ঠিক সেই মুহূর্তে থেমে commit করে নেবেন — তারপর পরের সেকশনে যাবেন। এটাই বাস্তব ডেভেলপমেন্ট অভ্যাস।
// vite.config.jsimport { 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 — এসব এখন আর দরকার নেই):
বর্তমান React Router (v8, ২০২৬-এ current release) থেকে react-router-dom প্যাকেজটা আর মূলধারায় নেই — BrowserRouter, Routes, Route, Link, useNavigate, useParams সবই সরাসরি react-router থেকে import হয়। এই টিউটোরিয়ালের সব জায়গায় আমরা "react-router" থেকেই import করব।
লক্ষ্য করুন — উপরে axios ইনস্টল করিনি। RTK Query আলাদা কোনো প্যাকেজ না — এটা @reduxjs/toolkit-এর ভেতরেই বিল্ট-ইন থাকে, @reduxjs/toolkit/query/react থেকে import হবে (সেকশন ৭-এ দেখবেন)। তাই ডাটা fetch করার জন্য axios বা fetch আলাদা করে ইনস্টল করার দরকার নেই।
json-server কী? — এটা একটা টুল যা একটা db.json ফাইল থেকে সম্পূর্ণ REST API বানিয়ে দেয় — কোনো Node/Express কোড না লিখেই GET/POST/PUT/DELETE সহ real HTTP endpoint পাওয়া যায়। শেখার আর প্রোটোটাইপিং-এর জন্য এটা দারুণ — production backend-এর জায়গায় ব্যবহার করবেন না, কিন্তু RTK Query শেখার জন্য এটা একদম পারফেক্ট, কারণ RTK Query real HTTP request-ই পাঠায়।
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 দিয়ে একটাই কমান্ডে দুটোই চালানো যাবে (সেকশন ২.৮-এ দেখবেন)।
এখানে একটা গুরুত্বপূর্ণ 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 গুলো লাগবে।
.gitignore-এ যোগ করুন যাতে .env কখনো commit না হয়:
# .gitignore-এ যোগ করুন# Environment variables.env
এই প্রজেক্টে .env-এ আসলে কোনো secret নেই (শুধু একটা local URL), কিন্তু অভ্যাসটা এখনই তৈরি করা ভালো — বড় প্রজেক্টে .env-এ API key, database URL-এর মতো sensitive তথ্য থাকবে, আর তখন এই অভ্যাসটাই আপনাকে বাঁচাবে।
এখন পর্যন্ত আমরা 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 ব্যবহার করিনি এমন কারো জন্য প্রথমে বুঝে নেওয়া দরকার — কেন এটা দরকার।
আমাদের Cart-এর data অনেক জায়গা থেকে দরকার হবে: Navbar-এ কার্টের আইটেম-সংখ্যা দেখাতে, ProductCard-এ "কার্টে যোগ করুন" বাটনে, CartPage-এ পুরো লিস্ট দেখাতে, CheckoutPage-এ অর্ডার বসাতে। এই সব কম্পোনেন্ট একে অপরের parent-child না — props দিয়ে data চালাচালি করলে (prop drilling) কোড অগোছালো হয়ে যাবে।
Redux-এর সমাধান: একটা central store — পুরো অ্যাপের একটাই global state, যেকোনো কম্পোনেন্ট থেকে সরাসরি পড়া আর পরিবর্তন করা যায়।
মূল চারটা concept:
Store — পুরো অ্যাপের state যেখানে থাকে। একটাই store থাকে।
Slice — state-এর একটা নির্দিষ্ট অংশ (যেমন cart), তার সাথে যুক্ত সব reducer একসাথে।
Action — "কী পরিবর্তন করতে চাই" তার বর্ণনা (যেমন addToCart)। Component থেকে dispatch(addToCart(product)) কল করে action পাঠানো হয়।
Reducer — action পেয়ে state আসলে কীভাবে বদলাবে সেই ফাংশন।
Flow: Component dispatch করে একটা action → সেই slice-এর reducer state আপডেট করে → যেই component useSelector দিয়ে সেই অংশটুকু subscribe করে আছে, সে automatically re-render হয়।
পুরনো (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।
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 নিয়ে শুরু হয়।
Deshi Book প্রজেক্টে ডাটা আনার জন্য আমরা লিখেছিলাম: useState দিয়ে data/loading/error তিনটা state, useEffect-এর ভেতরে axios call, isMounted flag, try/catch/finally। প্রতিটা API call-এর জন্য এই একই প্যাটার্ন বারবার লিখতে হতো।
RTK Query এই পুরো প্যাটার্নটাকে একটা মাত্র hook কলে নিয়ে আসে:
সহজভাবে বললে — RTK Query হলো "data fetching + caching লাইব্রেরি", যেটা Redux-এর উপরেই বানানো। এটা আপনার হয়ে API call করে, ফলাফল Redux store-এ cache করে রাখে, আর একটা React hook জেনারেট করে দেয় যেটা কল করলেই data, isLoading, isError — সবকিছু রেডি পাওয়া যায়।
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 দেখতে পাবেন।
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 hooks — getProducts এন্ডপয়েন্ট থেকে automatically useGetProductsQuery হুক তৈরি হয়ে যায় (নাম প্যাটার্ন: use + PascalCase এন্ডপয়েন্ট নাম + Query/Mutation)। এই হুকগুলোই component-এ ব্যবহার করব — নিজে কিছু বানাতে হবে না।
createOrder মিউটেশন বলছে — "আমি সফল হলে { type: 'Order', id: 'LIST' } ট্যাগটা invalidate (বাতিল) করে দাও" (invalidatesTags)।
যখনই 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 করো, পুরো লিস্ট না" — এই সূক্ষ্ম নিয়ন্ত্রণও সম্ভব হয়।
cartSlice (সেকশন ৪) আর productsApi (সেকশন ৭) — দুটোই এখন রেডি। এবার এদের একটা central store-এ একসাথে জোড়া লাগানোর পালা।
// src/app/store.jsimport { 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.jsximport { 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>,);
ধারণাগতভাবে হ্যাঁ — 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 শুরু করি।
পুরো অ্যাপে ৬টা পেজ থাকবে (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 করা।
বাস্তব প্রজেক্টে সাধারণত সব page একসাথে রেডি থাকে না। তাই আমরাও App.jsx-এ একবারে সবগুলো route যোগ না করে, প্রতিটা ফিচার শেষ হওয়ার সাথে সাথে তার route যোগ করব — Feature ১ শেষ হলে / route, Feature ২ শেষ হলে /cart route, ইত্যাদি। এটাই বাস্তবসম্মত: pages রেডি হওয়ার আগেই তাদের import করলে অ্যাপ ভেঙে যাবে (Failed to resolve import)। প্রতিটা ফিচার সেকশনের শেষে App.jsx-এর হালনাগাদ কোড আর তার commit দেখতে পাবেন।
useSelector(selectCartTotalQty) — এই একটা লাইনেই Navbar কার্টের মোট আইটেম-সংখ্যায় subscribe হয়ে গেল। cartSlice-এর যেকোনো reducer কার্ট বদলালেই, এই কম্পোনেন্ট automatically re-render হয়ে নতুন সংখ্যা দেখাবে — কোনো prop pass করা লাগছে না।
পুরো কার্ডটাই <Link>, আর ভেতরে "কার্টে যোগ করুন" বাটনও আছে। বাটনে ক্লিক করলে e.preventDefault() না দিলে ব্রাউজার প্রোডাক্ট ডিটেইলস পেজেও নিয়ে যাবে, আবার কার্টেও যোগ হবে — দুটো আচরণ মিলে যাবে। preventDefault() দিয়ে শুধু "কার্টে যোগ করা" অংশটাই ঘটবে, navigation হবে না।
শুধু প্রথমবার ডাটা fetch হওয়ার সময় (কোনো cache data-ই নেই তখনো)
isFetching
প্রথমবার fetch-এও true হয়, আবার category/search বদলে re-fetch হওয়ার সময়ও true হয়
isLoading দিয়ে প্রথম লোডে full-page loading message দেখানো হয়েছে, আর isFetching দিয়ে filter বদলানোর সময় পুরনো ডাটা আবছা (opacity কমিয়ে) দেখিয়ে রাখা হয়েছে — যাতে UI হঠাৎ খালি হয়ে না যায়। শুধু isLoading ব্যবহার করলে প্রতিবার filter বদলালেও পুরো গ্রিড হঠাৎ মুছে গিয়ে আবার loading text দেখাত — অভিজ্ঞতা খারাপ হতো।
এই পুরো ফিচারটা (Navbar + ৪টা ছোট কম্পোনেন্ট + HomePage + তার routing) একসাথে একটা commit — এভাবেই বাস্তব ডেভেলপাররা "ফিচার-লেভেল" commit করেন, প্রতিটা ছোট ফাইলের জন্য আলাদা commit না করে।
🔧 Git Commit
git add src/components src/pages src/App.jsxgit commit -m "feat: add product browsing - navbar, product card, grid, filters and home page"
useParams() দিয়ে URL-এর :id অংশ (যেমন /product/6) থেকে 6 বের করা হয়েছে, তারপর সরাসরি useGetProductByIdQuery(id)-তে পাঠানো হয়েছে — RTK Query নিজে থেকেই id বদলালে (অন্য প্রোডাক্টে ক্লিক করলে) নতুন করে fetch করবে।
লক্ষ্য করুন — cartItems আর totalPrice দুটোই সরাসরি cartSlice.js-এর selector থেকে আসছে। কোথাও useState, কোথাও useEffect নেই — Redux store-ই একমাত্র উৎস।
App.jsx-এ /cart route যোগ করুন:
// src/App.jsximport { 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;
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 রেখে দেয়।
// src/pages/CheckoutPage.jsximport { 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 পেজে পাঠানো হচ্ছে।
আপনি /orders পেজে যান — useGetOrdersQuery() চলে, { type: 'Order', id: 'LIST' } ট্যাগে subscribe হয়ে যায়, অর্ডার লিস্ট দেখায়।
অন্য ট্যাবে বা পরে /checkout-এ গিয়ে একটা অর্ডার প্লেস করেন — createOrder মিউটেশন সফল হয়, invalidatesTags: [{ type: 'Order', id: 'LIST' }] কার্যকর হয়।
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.jsximport { 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.jsxgit commit -m "feat: add order history page with cache invalidation demo"
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 নামের কোনো ফিচার নেই — একজন ইউজার চাইলে পরে কেনার জন্য প্রোডাক্ট আলাদা করে রাখতে পারছেন না।
নির্দেশনা:
src/features/wishlist/wishlistSlice.js নামে ঠিক cartSlice.js-এর মতো ধাঁচে একটা নতুন slice বানান — toggleWishlist (আছে থাকলে বাদ, না থাকলে যোগ), আর selectWishlistItems selector সহ।
store.js-এ নতুন slice-টা configureStore-এর reducer object-এ যোগ করুন।
ProductCard.jsx-এ একটা হার্ট (♥) আইকন বাটন যোগ করুন যেটা dispatch(toggleWishlist(product)) কল করবে — ঠিক "কার্টে যোগ করুন" বাটনের মতোই e.preventDefault() ব্যবহার করতে ভুলবেন না।
WishlistPage.jsx নামে নতুন একটা পেজ বানান (অনেকটা CartPage.jsx-এর মতো) যেটা selectWishlistItems থেকে আইটেম দেখাবে, আর App.jsx-এ /wishlist route যোগ করুন।
টেস্ট করুন — একটা প্রোডাক্ট wishlist-এ যোগ করুন, পেজ reload দিন (চাইলে wishlist-ও localStorage-এ persist করতে পারেন, cartSlice-এর প্যাটার্ন অনুসরণ করে), /wishlist-এ গিয়ে দেখাচ্ছে কিনা যাচাই করুন।
সবশেষে, ঠিক আমাদের বাকি ফিচারগুলোর মতোই এটাও commit করুন:
এই exercise আপনাকে independently আরেকটা সম্পূর্ণ Redux Toolkit slice বানানোর অনুশীলন দেবে — ঠিক Deshi Book প্রজেক্টের "own feed update" exercise-এর মতোই, কিন্তু এবার axios/reducer-এর বদলে Redux Toolkit দিয়ে, আর নিজের হাতে একটা real commit + push করার অনুশীলনও হয়ে যাবে।
✅ 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 করুন — শুধু কাজ শেষে না, ছোট ছোট মাইলফলকেই।
লোকাল রিপোজিটরি ব্যাকআপ রাখতে আর পোর্টফোলিওতে দেখানোর জন্য GitHub-এ push করুন।
github.com/new-এ গিয়ে একটা নতুন, খালি repository বানান (README/.gitignore না যোগ করে — আমাদের এগুলো আগে থেকেই আছে)।
লোকাল repo-র সাথে remote যুক্ত করুন:
git remote add origin https://github.com/<আপনার-ইউজারনেম>/deshi-dokan.gitgit push -u origin main
এরপর থেকে প্রতিটা নতুন commit-এর পর শুধু:
git push
এখন git log --oneline চালালে দেখা উচিত (উপর থেকে নিচে, সবচেয়ে নতুন কমিট সবার উপরে):
docs: add project readmefeat: add order history page with cache invalidation demofeat: add checkout flow with rtk query mutationfeat: add shopping cart pagefeat: add product details pagefeat: add product browsing - navbar, product card, grid, filters and home pagefeat: configure redux store and wrap app with providerfeat: add products api slice with rtk queryfeat: add cart redux slice with localStorage persistencechore: setup redux toolkit, react-router and mock backend with json-serverstyle: add tailwind csschore: scaffold project with vite and react
১২টা commit — প্রতিটাই একটা নির্দিষ্ট, স্বনির্ভর পরিবর্তনের প্রতিনিধিত্ব করছে। GitHub-এ repo-র "Commits" ট্যাবে গেলে ঠিক এই history-টাই দেখতে পাবেন, প্রতিটা commit ক্লিক করলে তার নির্দিষ্ট diff-ও দেখা যাবে।
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।
অর্ডার হিস্ট্রি (ফিচার ৪, বোনাস): 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 অনুযায়ী বদলে নিলেই হবে।