feature
Creating a new feature module in the React web frontend. Triggers when adding a new page, creating a feature folder under src/features/, implementing a new domain area (auth, admin, manager, staff), o
SKILL.md.codex/skills/fe/feature/SKILL.md
GitHubNguyên tắc bắt buộc
- Tuân thủ tuyệt đối cấu trúc folder, naming convention, và code pattern được định nghĩa trong skill này — không tự ý thêm layer, đổi tên file, hay gọi API trực tiếp trong component.
- Hỏi trước khi code nếu bất kỳ yêu cầu nào chưa rõ (feature thuộc domain nào, data shape từ API, role nào được truy cập). Không đoán mò — một câu hỏi ngắn tốt hơn một giờ implement sai hướng.
FE Feature Pattern
Feature folder structure
src/features/{domain}/
├── pages/
│ └── {Name}Page.tsx
├── components/
│ └── {Name}.tsx
├── hooks/
│ └── use{Name}.ts
├── services/
│ └── {name}.service.ts
├── schemas/
│ └── {name}.schema.ts
└── types/
└── {name}.types.ts
Page component
const BatteryListPage: React.FC = () => {
const { data, isLoading, error } = useBatteries();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorBoundary error={error} />;
return (
<div>
{data?.items.map(b => <BatteryCard key={b.id} battery={b} />)}
</div>
);
};
export default BatteryListPage;
Service layer
// src/features/admin/services/battery.service.ts
import { axiosInstance } from '@/shared/lib/axios';
import type { Battery, BatteryCreatePayload } from '../types/battery.types';
import type { PaginationResponse } from '@/shared/types/api.types';
const BASE = '/api/batteries';
export const batteryService = {
getList: (params: BatteryGetListParams) =>
axiosInstance.get<PaginationResponse<Battery>>(BASE, { params }).then(r => r.data),
getById: (id: string) =>
axiosInstance.get<Battery>(`${BASE}/${id}`).then(r => r.data),
create: (payload: BatteryCreatePayload) =>
axiosInstance.post<Battery>(BASE, payload).then(r => r.data),
update: (id: string, payload: Partial<BatteryCreatePayload>) =>
axiosInstance.put<Battery>(`${BASE}/${id}`, payload).then(r => r.data),
delete: (id: string) =>
axiosInstance.delete(`${BASE}/${id}`).then(r => r.data),
};
Naming
| Type | Pattern | Example |
|---|---|---|
| Page | {Name}Page.tsx |
BatteryListPage.tsx |
| Component | {Name}.tsx |
BatteryCard.tsx |
| Hook | use{Name}.ts |
useBatteries.ts |
| Service | {name}.service.ts |
battery.service.ts |
| Schema | {name}.schema.ts |
battery.schema.ts |
| Types | {name}.types.ts |
battery.types.ts |
Zod schema (form validation)
// src/features/admin/schemas/battery.schema.ts
import { z } from 'zod';
export const batteryCreateSchema = z.object({
name: z.string().min(1, 'Required').max(100, 'Max 100 chars'),
locationId: z.string().uuid('Invalid ID').optional(),
});
export type BatteryCreateFormValues = z.infer<typeof batteryCreateSchema>;
// trong component form — kết hợp react-hook-form + zod
const form = useForm<BatteryCreateFormValues>({
resolver: zodResolver(batteryCreateSchema),
});
Rules
- Never call API in component — always through
services/→ TanStack Query hook useStateonly for UI state (modal, tab, toggle)- Zustand only for auth session — not server state
- Features must NOT import from other features — only from
shared/ - Never hardcode URLs — use
env.VITE_API_BASE_URL - Never create a new Axios instance — use
shared/lib/axios.ts - Never store token in localStorage — use
js-cookie