Code giỏ hàng với Context API + Hook

Context API + Hook + Typescript

Context API, Hook thì quá nổi tiếng rồi, chắc trên mạng đầy bài giới thiệu rồi nên cháu lười viết lắm =)). Mặc dù cháu thấy có rất nhiều bài hướng dẫn trên mạng nhưng lúc cháu tìm trên Google tuyệt nhiên không có bài nào viết CartContext bằng Typescript. Mà tính cháu do quen ở công ty cũ, viết JS mà không có Typescript thấy nó ngu người lắm nên tính lên bài này cho các bác nào cần.

Dân chơi đọc Cart phát biết làm gì luôn đúng không các bác, một cái giỏ hàng có thêm sửa xoá sản phẩm (lineItems) trong đấy, thêm linh tinh mấy hàm như tính tổng.

Bắt đầu nào

Đầu tiên, các bác hình dung sơ qua những hàm sẽ có để viết interface CartContextState

interface CartContextState {
  lineItems: ILineItem[]; // Tuỳ các bác thiết kế thế nào nha
  addToCart: (item: IProduct) => void;
  removeFromCart: (item: IProduct) => void;
  cartSum: () => number;
  clearCart: () => void;
  cartCount: () => number;
}

Sau đó tạo context CartContext sử dụng hàm React.createContext:

const CartContext = React.createContext<CartContextState | undefined>(
  undefined,
);

và tạo hook cho cái context này, đây sẽ là nơi các bác lấy dữ liệu từ context

export function useCart(): CartContextState {
  const context = useContext<any>(CartContext);

  if (!context) {
    console.log('Chưa bọc App vào trong cái CartProvider thì chưa dùng được nha..');
  }
  return context;
}

sau đó giờ các bác phải viết Provider để bọc cái App vào trong, lúc đó dữ liệu trong context mới được chia sẻ giữa các components con của nó


interface CartProviderProps {
  children: React.ReactNode;
}

export const CartProvider: React.FunctionComponent<CartProviderProps> = ({
  children,
}: CartProviderProps) => {
  const [lineItems, setLineItems] = useState<ILineItem[]>([]);
  
  return (
    <CartContext.Provider
      value=>
      {children}
    </CartContext.Provider>
  );
};

Ví dụ cháu sẽ cài đặt các hàm trên như sau

  const addToCart = (item: IProduct) => {
    if (lineItems) {
      let newLineItems = [];
      let oldData = lineItems;
      let check = oldData.findIndex((_item) => {
        return item.id === _item.product.id;
      });
      if (check === -1) {
        const newData: ILineItem = {
          product: item,
          quantity: 1,
          timestamp: Date.now(),
        };
        newLineItems = [...lineItems, newData];
      } else {
        newLineItems = [
          ...oldData.filter((_i) => _i.product.id !== item.id),
          {
            product: oldData[check].product,
            quantity: (oldData[check].quantity += 1),
            timestamp: oldData[check].timestamp,
          },
        ];
      }
      setLineItems(
        newLineItems
          .sort(function (a, b) {
            return a.timestamp - b.timestamp;
          })
          .reverse(),
      );
    }
  };

  const clearCart = () => {
    setLineItems([]);
  };

  const cartSum = () => {
    let sum = 0;
    lineItems?.forEach((item) => (sum += item.quantity * item.product.price));
    return sum;
  };

  const cartCount = () => {
    let sum = 0;
    lineItems?.forEach((item) => (sum += item.quantity));
    return sum;
  };

Xong rồi ở file App các bác bọc nó kiểu kiểu thế này ha

<CartProvider>
  <App />
</CartProvider>

Lúc đó tha hồ ở trong các nút con của App, muốn lấy lineItems chỉ việc gọi

const { lineItems } = useCart();

Full Code không che:

interface CartProviderProps {
  children: React.ReactNode;
}

export interface ILineItem {
  product: IProduct;
  quantity: number;
  timestamp: number;
}

interface CartContextState {
  lineItems?: ILineItem[];
  addToCart: (item: IProduct) => void;
  removeFromCart: (item: IProduct) => void;
  cartSum: () => number;
  clearCart: () => void;
  cartCount: () => void;
}

const CartContext = React.createContext<CartContextState | undefined>(
  undefined,
);

export const CartProvider: React.FunctionComponent<CartProviderProps> = ({
  children,
}: CartProviderProps) => {
  const [lineItems, setLineItems] = useState<ILineItem[]>([]);

  const addToCart = (item: IProduct) => {
    if (lineItems) {
      let newLineItems = [];
      let oldData = lineItems;
      let check = oldData.findIndex((_item) => {
        return item.id === _item.product.id;
      });
      if (check === -1) {
        const newData: ILineItem = {
          product: item,
          quantity: 1,
          timestamp: Date.now(),
        };
        newLineItems = [...lineItems, newData];
      } else {
        newLineItems = [
          ...oldData.filter((_i) => _i.product.id !== item.id),
          {
            product: oldData[check].product,
            quantity: (oldData[check].quantity += 1),
            timestamp: oldData[check].timestamp,
          },
        ];
      }
      setLineItems(
        newLineItems
          .sort(function (a, b) {
            return a.timestamp - b.timestamp;
          })
          .reverse(),
      );
    }
  };

  const removeFromCart = (item: IProduct) => {
    setLineItems(lineItems.filter((_item) => _item.product.id !== item.id));
  };

  const clearCart = () => {
    setLineItems([]);
  };

  const cartSum = () => {
    let sum = 0;
    lineItems?.forEach((item) => (sum += item.quantity * item.product.price));
    return sum;
  };

  const cartCount = () => {
    let sum = 0;
    lineItems?.forEach((item) => (sum += item.quantity));
    return sum;
  };

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

export function useCart(): CartContextState {
  const context = useContext<any>(CartContext);

  if (!context) {
    console.log('useCart not wrapped');
  }
  return context;
}