Advance React শিখুন বাংলায় (পর্ব ৮) — Mini Project: Global State-সহ Shopping Cart

Mini Project - Global State-সহ Shopping Cart Dashboard

Advanced React সিরিজের সাত পর্বে আমরা শিখেছি useReducer, Context API, useMemo, useCallback, Custom Hook, React.memo, আর Error Boundary/Code Splitting। এই শেষ পর্বে সবকিছু একসাথে মিলিয়ে বানাব একটা Global State-সহ শপিং কার্ট ড্যাশবোর্ড — যেখানে কার্টের ডেটা পুরো অ্যাপ জুড়ে অ্যাক্সেসযোগ্য, আর পারফরম্যান্স-সচেতনভাবে লেখা।

প্রজেক্টে যা যা প্রয়োগ হচ্ছে

  • useReducer (পর্ব ১) — কার্টের সব action (যোগ, বাদ, কোয়ান্টিটি) একটা reducer-এ
  • Context API (পর্ব ২) — কার্ট state prop drilling ছাড়াই পুরো অ্যাপে অ্যাক্সেসযোগ্য
  • useMemo (পর্ব ৩) — কার্টের টোটাল দাম শুধু দরকার হলেই আবার গণনা
  • useCallback (পর্ব ৪) — dispatch-ভিত্তিক ফাংশনগুলো স্থিতিশীল রেফারেন্স রাখা
  • Custom Hook (পর্ব ৫)useCart() দিয়ে সহজ অ্যাক্সেস
  • React.memo (পর্ব ৬) — প্রোডাক্ট কার্ড অপ্রয়োজনীয় re-render থেকে সুরক্ষিত
  • Error Boundary (পর্ব ৭) — কার্ট প্যানেল ক্র্যাশ করলেও বাকি অ্যাপ ঠিক থাকবে

ধাপ ১ — Cart Reducer (পর্ব ১-এর প্যাটার্ন)

// cartReducer.js
export function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const existing = state.find((i) => i.id === action.payload.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.payload.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.payload, qty: 1 }];
    }
    case "REMOVE_ITEM":
      return state.filter((i) => i.id !== action.payload.id);
    case "UPDATE_QTY":
      return state.map((i) =>
        i.id === action.payload.id
          ? { ...i, qty: Math.max(1, action.payload.qty) }
          : i
      );
    case "CLEAR_CART":
      return [];
    default:
      return state;
  }
}

ধাপ ২ — CartContext + useCart (পর্ব ২ ও ৫-এর মিশ্রণ)

Context আর Custom Hook একসাথে — যেকোনো component থেকে useCart() কল করলেই কার্ট, দাম, আর সব action পাওয়া যাবে।

// CartContext.js
import { createContext, useContext, useReducer, useMemo, useCallback } from "react";
import { cartReducer } from "./cartReducer.js";

const CartContext = createContext();

export function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, []);

  // useMemo (পর্ব ৩) — cart না পাল্টালে totalPrice আবার গণনা হবে না
  const totalPrice = useMemo(() => {
    return cart.reduce((sum, item) => sum + item.price * item.qty, 0);
  }, [cart]);

  const totalItems = useMemo(() => {
    return cart.reduce((sum, item) => sum + item.qty, 0);
  }, [cart]);

  // useCallback (পর্ব ৪) — প্রতিটা ফাংশনের রেফারেন্স স্থিতিশীল
  const addItem = useCallback((product) => {
    dispatch({ type: "ADD_ITEM", payload: product });
  }, []);

  const removeItem = useCallback((id) => {
    dispatch({ type: "REMOVE_ITEM", payload: { id } });
  }, []);

  const updateQty = useCallback((id, qty) => {
    dispatch({ type: "UPDATE_QTY", payload: { id, qty } });
  }, []);

  const clearCart = useCallback(() => {
    dispatch({ type: "CLEAR_CART" });
  }, []);

  const value = { cart, totalPrice, totalItems, addItem, removeItem, updateQty, clearCart };

  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error("useCart অবশ্যই CartProvider-এর ভেতরে ব্যবহার করতে হবে");
  }
  return context;
}

ধাপ ৩ — Memoized ProductCard (পর্ব ৬)

// ProductCard.jsx
import { memo } from "react";
import { useCart } from "./CartContext.js";

const ProductCard = memo(function ProductCard({ product }) {
  const { addItem } = useCart(); // addItem স্থিতিশীল, তাই এই component অপ্রয়োজনে re-render হবে না

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>৳{product.price}</p>
      <button onClick={() => addItem(product)}>কার্টে যোগ করো</button>
    </div>
  );
});

export default ProductCard;

ধাপ ৪ — CartPanel, Error Boundary দিয়ে সুরক্ষিত (পর্ব ৭)

// ErrorBoundary.jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) {
      return <p>কার্ট প্যানেলে সমস্যা হয়েছে। বাকি সাইট ঠিকই কাজ করছে।</p>;
    }
    return this.props.children;
  }
}
// CartPanel.jsx
import { useCart } from "./CartContext.js";

function CartPanel() {
  const { cart, totalPrice, totalItems, removeItem, updateQty, clearCart } = useCart();

  if (cart.length === 0) {
    return <p>কার্ট খালি</p>;
  }

  return (
    <div>
      <h3>কার্ট ({totalItems}টা আইটেম)</h3>
      {cart.map((item) => (
        <div key={item.id}>
          {item.name} × {item.qty} = ৳{item.price * item.qty}
          <button onClick={() => removeItem(item.id)}>মুছো</button>
        </div>
      ))}
      <p>মোট: ৳{totalPrice}</p>
      <button onClick={clearCart}>কার্ট খালি করো</button>
    </div>
  );
}

ধাপ ৫ — সবকিছু একসাথে (App)

// App.jsx
import { CartProvider } from "./CartContext.js";
import ProductGrid from "./ProductGrid.jsx";
import CartPanel from "./CartPanel.jsx";
import ErrorBoundary from "./ErrorBoundary.jsx";

function App() {
  return (
    <CartProvider>
      <div>
        <ProductGrid />
        <ErrorBoundary>
          <CartPanel />
        </ErrorBoundary>
      </div>
    </CartProvider>
  );
}

ProductGrid-এর ভেতরের সব ProductCard কোনো prop drilling ছাড়াই useCart() দিয়ে সরাসরি কার্টে যোগ করতে পারছে, আর CartPanel Error Boundary দিয়ে সুরক্ষিত — এটা ভেঙে গেলেও প্রোডাক্ট গ্রিড ঠিকই দেখা যাবে।

লাইভ ডেমো — সম্পূর্ণ কার্ট সিস্টেম

নিচে সবকিছু একসাথে কাজ করছে — প্রোডাক্ট কার্ড থেকে যোগ করো, কার্ট প্যানেল থেকে কোয়ান্টিটি বদলাও বা মুছে ফেলো। প্রোডাক্ট কার্ডগুলো React.memo দিয়ে সুরক্ষিত, তাই কার্ট প্যানেল আপডেট হলেও কার্ডগুলো re-render হয় না।

প্রতিটা প্রোডাক্ট কার্ডে "render: N" সংখ্যাটা লক্ষ্য করো — কার্ট প্যানেলে কোয়ান্টিটি বদলালেও এই সংখ্যা বাড়ে না, কারণ React.memo আর useCallback মিলে অপ্রয়োজনীয় re-render আটকাচ্ছে।

কোড কীভাবে সিরিজের সব শেখা মেলাচ্ছে — সংক্ষেপে

প্রজেক্টের অংশ কোন পর্বের কনসেপ্ট
cartReducer ও action/dispatch পর্ব ১ (useReducer)
CartContext.Provider, prop drilling এড়ানো পর্ব ২ (Context API)
totalPrice, totalItems useMemo পর্ব ৩ (useMemo)
addItem, removeItem useCallback পর্ব ৪ (useCallback)
useCart() Custom Hook পর্ব ৫ (Custom Hook)
memo(ProductCard) পর্ব ৬ (React.memo)
ErrorBoundary-এ CartPanel wrap করা পর্ব ৭ (Error Boundary)

পরবর্তী উন্নতির আইডিয়া

Exercise: প্রজেক্টটা আরও ভালো করার জন্য নিজে চেষ্টা করো —

  1. React এর হাতেখড়ি পর্ব ৯-এর useLocalStorage দিয়ে কার্ট রিফ্রেশের পরও ধরে রাখো (Custom Hook একসাথে মেলানো, পর্ব ৫-এর প্যাটার্ন)।
  2. React.lazy দিয়ে CartPanel lazy-load করো, শুধু ব্যবহারকারী কার্ট আইকনে ক্লিক করলেই লোড হবে।
  3. React Router (পর্ব ১৫) দিয়ে একটা আলাদা checkout পেজ বানাও, যেখানেও useCart() ব্যবহার হবে।
  4. React DevTools Profiler (পর্ব ৬) দিয়ে যাচাই করো, সত্যিই ProductCard-গুলো কার্ট আপডেটে re-render হচ্ছে না।

সিরিজ শেষ!

Advanced React সিরিজের আট পর্বে আমরা useReducer থেকে শুরু করে Context, useMemo/useCallback, Custom Hook, React.memo, Error Boundary, আর Code Splitting পর্যন্ত শিখেছি, আর আজ সেগুলো একসাথে মিলিয়ে একটা production-মানের প্যাটার্ন দিয়ে একটা কার্ট সিস্টেম বানিয়েছি। HTML থেকে শুরু করে React পর্যন্ত পুরো এই যাত্রায় শেখা সবকিছু এখন তোমার যেকোনো বাস্তব প্রজেক্টে প্রয়োগ করার জন্য প্রস্তুত।

একটি মন্তব্য পোস্ট করুন

নবীনতর পূর্বতন

যোগাযোগ ফর্ম