Mục lục

Foundation: Shared Logic & Utils

Xây dựng thư viện @repo/utils để chia sẻ logic xử lý giá tiền, ngày tháng và validation schema giữa các dự án.

Ngoài UI, một dự án E-commerce có rất nhiều logic nghiệp vụ lặp lại. Nếu copy-paste code từ Admin sang Store, bạn đang tạo ra nợ kỹ thuật (Technical Debt).

1. Cấu trúc packages/utils

Code:
packages/utils/
  ├── src/
  │   ├── currency.ts
  │   ├── date.ts
  │   └── index.ts
  ├── package.json
  └── tsconfig.json

Format tiền tệ (Currency)

Tiền Việt Nam có quy tắc riêng. Đừng hardcode vnd ở khắp nơi.

typescript:
// packages/utils/src/currency.ts
export const formatPrice = (price: number | string) => {
  const numericPrice = typeof price === "string" ? parseFloat(price) : price;
  
  return new Intl.NumberFormat("vi-VN", {
    style: "currency",
    currency: "VND",
    maximumFractionDigits: 0,
  }).format(numericPrice);
};

Date Parsing (date-fns wrapper)

Thống nhất format ngày tháng dd/MM/yyyy toàn hệ thống.

typescript:
// packages/utils/src/date.ts
import { format, parseISO } from "date-fns";
import { vi } from "date-fns/locale";

export const formatDate = (date: string | Date) => {
  const d = typeof date === "string" ? parseISO(date) : date;
  return format(d, "dd 'tháng' MM, yyyy", { locale: vi });
};

2. Shared Zod Schemas (@repo/schema)

Validation logic là thứ quan trọng nhất cần share. Backend dùng nó để validate API body, Frontend dùng nó để validate Form.

typescript:
// packages/schema/src/auth.ts
import { z } from "zod";

export const loginSchema = z.object({
  email: z.string().email("Email không hợp lệ"),
  password: z.string().min(6, "Mật khẩu tối thiểu 6 ký tự"),
});

export const registerSchema = loginSchema.extend({
  fullName: z.string().min(2, "Tên phải dài hơn 2 ký tự"),
});

export type LoginInput = z.infer<typeof loginSchema>;

Sử dụng ở Frontend (React Hook Form)

typescript:
import { loginSchema } from "@repo/schema";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const form = useForm({
  resolver: zodResolver(loginSchema)
});

Sử dụng ở Backend (Server Action)

typescript:
import { loginSchema } from "@repo/schema";

export async function loginAction(formData: FormData) {
  const raw = Object.fromEntries(formData);
  const result = loginSchema.safeParse(raw);
  
  if (!result.success) {
    return { error: "Dữ liệu không hợp lệ" };
  }
}

3. Custom Hooks (@repo/hooks)

Chia sẻ logic React.

typescript:
// packages/hooks/use-debounce.ts
import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

Tổng kết

Việc tách logic ra khỏi UI Component giúp:

  1. Dễ test: Viết Unit Test cho formatPrice dễ hơn test cả cái component ProductCard.
  2. Consistency: 1 tỷ đồng luôn hiển thị là 1.000.000.000 ₫ chứ không phải 1000tr hay 1 tỷ.
  3. Type Safety: Schema được đồng bộ giữa FE và BE.
Quảng cáo
mdhorizontal