Tiêu Chuẩn Code — Frontend & Workflow
| Dự Án | PTX Channel Manager (ptx-cm) |
| Cập Nhật | 2026-02-20 |
| Xem thêm | code-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ại | Dùng Cho |
|---|---|
feat | Tính năng mới |
fix | Sửa lỗi |
docs | Chỉ tài liệu |
style | Định dạng, không thay đổi logic |
refactor | Tái cấu trúc code, không thay đổi tính năng |
test | Thêm hoặc cập nhật test |
chore | Build, 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 compileKhông bao giờ commit:
- File
.envhoặ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 con4. Tài Liệu Liên Quan
- code-standards.md — Nguyên tắc, đặt tên, quy tắc kích thước file
- code-standards-backend.md — NestJS, bảo mật, DB, testing
- system-architecture.md — Sơ đồ kiến trúc
- UI_SPEC.md — Design system và đặc tả screen