Skip to content

Tiêu Chuẩn Code — Frontend & Workflow

Dự ÁnPTX Channel Manager (ptx-cm)
Cập Nhật2026-02-20
Xem thêmcode-standards.md (nguyên tắc & đặt tên)

1. Tiêu Chuẩn Frontend (Next.js/React)

Cấu Trúc File Component

typescript
// ✓ Tốt: Props có kiểu, tách biệt concerns, named export
'use client';

import { useForm } from 'react-hook-form';

interface PropertyFormProps {
  onSubmit: (data: CreatePropertyDto) => Promise<void>;
  loading?: boolean;
}

export function PropertyForm({ onSubmit, loading }: PropertyFormProps) {
  const { register, handleSubmit, formState: { errors } } = useForm<CreatePropertyDto>();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      <button type="submit" disabled={loading}>Tạo</button>
    </form>
  );
}

// ✗ Xấu: Tất cả logic inline, không có kiểu, chỉ default export
export default function Page() {
  const [properties, setProperties] = useState([]);
  const [loading, setLoading] = useState(false);
  const [formData, setFormData] = useState({});
  // ... 300 dòng logic và JSX inline
}

Data Fetching: SWR qua useApiGet

typescript
// ✓ Tốt: SWR hook với dedup + loading state
import { useApiGet } from '@/lib/use-api';

export function PropertyList() {
  const { data: properties, isLoading } = useApiGet<Property[]>('properties', {
    dedupingInterval: 30_000,
  });

  if (isLoading) return <LoadingSkeleton />;
  if (!properties?.length) return <EmptyState />;

  return <DataTable data={properties} columns={columns} />;
}

// ✗ Xấu: Fetch inline trong useEffect, không dedup
export function PropertyList() {
  const [properties, setProperties] = useState([]);

  useEffect(() => {
    fetch('/api/v1/properties')
      .then(r => r.json())
      .then(setProperties); // ❌ Không loading state, không dedup, không xử lý lỗi
  }, []);
}

Mẫu API Client

typescript
// ✓ Tốt: apiFetch tập trung với auto-retry 401
import { apiFetch } from '@/lib/api-client';

export const propertyApi = {
  list: (params?: ListPropertiesDto) => apiFetch<PaginatedResponse<Property>>('properties', { params }),
  create: (dto: CreatePropertyDto) => apiFetch<Property>('properties', { method: 'POST', body: dto }),
  get: (id: string) => apiFetch<Property>(`properties/${id}`),
};

// ✗ Xấu: Fetch inline lặp lại giữa các component
export function PropertyList() {
  useEffect(() => {
    fetch('/api/v1/properties') // ❌ Lặp ở nhiều nơi, không xử lý lỗi
      .then(r => r.json())
      .then(setProperties);
  }, []);
}

Sử Dụng Context Provider

typescript
// ✓ Tốt: Dùng hook context có kiểu
import { useAuth } from '@/lib/auth-context';
import { useCountry } from '@/lib/country-context';

export function BookingList() {
  const { user } = useAuth();
  const { country } = useCountry();

  const { data } = useApiGet<Booking[]>(`bookings?country=${country}`);
  // ...
}

// ✗ Xấu: Đọc context trực tiếp qua useContext
import { AuthContext } from '@/lib/auth-context';

export function BookingList() {
  const ctx = useContext(AuthContext); // ❌ Không kiểm tra null, không có kiểu
  const user = ctx?.user;
}

Kiểm Tra Quyền Hạn

typescript
// ✓ Tốt: Helper hasPermission từ gói types
import { hasPermission, Permission } from '@ptx-cm/types';

export function PropertyActions({ property }: { property: Property }) {
  const { user } = useAuth();
  const canEdit = hasPermission(user.permissions, 'PROPERTIES', Permission.EDIT);
  const canDelete = hasPermission(user.permissions, 'PROPERTIES', Permission.DELETE);

  return (
    <>
      {canEdit && <button onClick={onEdit}>Sửa</button>}
      {canDelete && <button onClick={onDelete}>Xóa</button>}
    </>
  );
}

// ✗ Xấu: Kiểm tra vai trò hardcoded
export function PropertyActions() {
  const { user } = useAuth();
  if (user.roleName === 'manager') { // ❌ Mong manh, bỏ qua bitmask quyền hạn
    return <button>Sửa</button>;
  }
}

2. Tiêu Chuẩn Git Commit

Conventional Commits

<type>(<scope>): <subject>

<body>

<footer>

Loại:

LoạiDùng Cho
featTính năng mới
fixSửa lỗi
docsChỉ tài liệu
styleĐịnh dạng, không thay đổi logic
refactorTái cấu trúc code, không thay đổi tính năng
testThêm hoặc cập nhật test
choreBuild, CI, cập nhật dependency

Ví Dụ:

bash
# ✓ Tốt
git commit -m "feat(auth): add refresh token revocation on logout"
git commit -m "fix(properties): prevent cross-country property access"
git commit -m "docs: update API_SPEC with country scoping details"
git commit -m "refactor(sync-engine): extract availability calc into separate service"

# ✗ Xấu
git commit -m "fix stuff"
git commit -m "Update code"
git commit -m "wip"

Checklist Trước Push

bash
pnpm lint        # Không lỗi
pnpm test        # Tất cả test pass
pnpm build       # Không lỗi compile

Không bao giờ commit:

  • File .env hoặc API key
  • node_modules/, dist/, .next/
  • File Prisma client đã generate

3. Hướng Dẫn Hiệu Suất

Kích Thước Response API

typescript
// ✓ Tốt: Chỉ select trường cần cho list view
const bookings = await this.prisma.booking.findMany({
  select: {
    id: true,
    otaBookingId: true,
    guestName: true,
    checkIn: true,
    checkOut: true,
    status: true,
    // Loại trừ: rawData, syncHistory (trường lớn chỉ cho detail view)
  },
});

// ✗ Xấu: Trả tất cả trường bao gồm nested data lớn
const bookings = await this.prisma.booking.findMany();
// Response bao gồm rawData (JSON response OTA đầy đủ), mảng syncHistory, v.v.

Luôn Phân Trang

typescript
// ✓ Tốt: Mặc định 25, tối đa 100
@Get()
async findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(25), ParseIntPipe) limit: number,
) {
  // Giới hạn 100 để ngăn lạm dụng
  const safeLimit = Math.min(limit, 100);
  return this.service.findAll(page, safeLimit);
}

Frontend: Tránh Re-render Không Cần Thiết

typescript
// ✓ Tốt: Tham chiếu callback ổn định
const updateUser = useCallback((updated: Partial<User>) => {
  setUser(prev => prev ? { ...prev, ...updated } : null);
}, []);

// ✓ Tốt: Memo dữ liệu dẫn xuất
const overdueAlerts = useMemo(
  () => alerts?.filter(a => !a.resolvedAt) ?? [],
  [alerts],
);

// ✗ Xấu: Inline function tạo lại mỗi render
<button onClick={() => updateUser({ locale: 'vi' })}>
  Chuyển sang tiếng Việt
</button>
// ❌ Tham chiếu hàm mới mỗi render gây re-render component con

4. Tài Liệu Liên Quan

PTX Channel Manager — Tài Liệu Nội Bộ