TTMT.ManageWebGUI/SERVICES_VS_HOOKS.md
2025-12-22 14:53:19 +07:00

9.3 KiB

Khác biệt giữa Services và Query Hooks

Tóm tắt nhanh

Aspect Services Query Hooks
Location src/services/ src/hooks/queries/
Mục đích Gọi API trực tiếp Wrapper TanStack Query
Caching Không
Background Refetch Không
Auto Invalidation Không
Type Async functions React Hooks
Dùng trong Non-React code, utilities React components

Chi tiết Từng Layer

1. Services Layer (src/services/)

Mục đích: Đơn thuần gọi API và trả về dữ liệu

// app-version.service.ts
export async function getSoftwareList(): Promise<Version[]> {
  const response = await axios.get<Version[]>(
    API_ENDPOINTS.APP_VERSION.GET_SOFTWARE
  );
  return response.data;
}

Đặc điểm:

  • Pure async functions
  • Không phụ thuộc vào React
  • Có thể sử dụng ở bất kỳ đâu (utils, servers, non-React code)
  • Không có caching
  • Phải tự quản lý state loading/error
  • Phải tự gọi lại khi dữ liệu thay đổi

Khi nào dùng:

// Dùng trong utility functions
export async function initializeApp() {
  const software = await appVersionService.getSoftwareList();
  // ...
}

// Hoặc trong services khác
export async function validateSoftware() {
  const list = await appVersionService.getSoftwareList();
  // ...
}

2. Query Hooks Layer (src/hooks/queries/)

Mục đích: Wrapper TanStack Query bên trên services

// useAppVersionQueries.ts
export function useGetSoftwareList(enabled = true) {
  return useQuery<Version[]>({
    queryKey: ["app-version", "software"],
    queryFn: () => appVersionService.getSoftwareList(),
    enabled,
    staleTime: 60 * 1000, // 1 minute
  });
}

Đặc điểm:

  • React hooks
  • Automatic caching
  • Background refetching
  • Automatic invalidation sau mutations
  • Built-in loading/error states
  • Deduplication (gộp requests giống nhau)
  • Chỉ dùng được trong React components
  • Phức tạp hơn services

Khi nào dùng:

// Dùng trong React components
function SoftwareList() {
  const { data: software, isLoading } = useGetSoftwareList()
  
  if (isLoading) return <div>Loading...</div>
  return software?.map(item => <div>{item.name}</div>)
}

So sánh cụ thể

Ví dụ 1: Lấy danh sách

Service - Raw API call:

// services/app-version.service.ts
export async function getSoftwareList(): Promise<Version[]> {
  const response = await axios.get(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE);
  return response.data;
}

Hook - TanStack Query wrapper:

// hooks/queries/useAppVersionQueries.ts
export function useGetSoftwareList(enabled = true) {
  return useQuery({
    queryKey: ["app-version", "software"],
    queryFn: () => appVersionService.getSoftwareList(),
    staleTime: 60 * 1000,
  });
}

Sử dụng trong component:

function Component() {
  // ❌ KHÔNG nên dùng service trực tiếp
  const [data, setData] = useState<Version[]>([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    appVersionService.getSoftwareList().then(d => {
      setData(d);
      setLoading(false);
    });
  }, []);

  // ✅ NÊN dùng hook thay vì
  const { data, isLoading } = useGetSoftwareList();
}

Ví dụ 2: Upload file

Service:

// services/app-version.service.ts
export async function uploadSoftware(
  formData: FormData,
  onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
): Promise<{ message: string }> {
  return axios.post(API_ENDPOINTS.APP_VERSION.UPLOAD, formData, {
    onUploadProgress,
  });
}

Hook:

// hooks/queries/useAppVersionQueries.ts
export function useUploadSoftware() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: { formData: FormData; onUploadProgress?: ... }) =>
      appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
    onSuccess: () => {
      // Tự động invalidate software list
      queryClient.invalidateQueries({
        queryKey: ["app-version", "software"],
      });
    },
  });
}

Sử dụng:

function UploadForm() {
  const uploadMutation = useUploadSoftware();

  const handleUpload = async (file: File) => {
    const formData = new FormData();
    formData.append("file", file);
    
    await uploadMutation.mutateAsync({
      formData,
      onUploadProgress: (e) => console.log(`${e.loaded}/${e.total}`)
    });
    
    // ✅ Software list tự động update
  };

  return (
    <button disabled={uploadMutation.isPending}>
      {uploadMutation.isPending ? "Uploading..." : "Upload"}
    </button>
  );
}

Architecture Flow

┌─────────────────────────────────────────┐
│         React Component                  │
│   (SoftwareList, UploadForm, etc.)      │
└──────────────┬──────────────────────────┘
               │
               │ uses
               ▼
┌─────────────────────────────────────────┐
│   Query Hooks (src/hooks/queries/)      │
│  - useGetSoftwareList()                 │
│  - useUploadSoftware()                  │
│  - useDeleteBlacklist()                 │
│  - Features:                            │
│    - Caching                            │
│    - Auto invalidation                  │
│    - Loading states                     │
└──────────────┬──────────────────────────┘
               │
               │ wraps
               ▼
┌─────────────────────────────────────────┐
│   Service Functions (src/services/)     │
│  - getSoftwareList()                    │
│  - uploadSoftware()                     │
│  - deleteBlacklist()                    │
│  - Features:                            │
│    - Pure async functions               │
│    - Direct API calls                   │
└──────────────┬──────────────────────────┘
               │
               │ uses
               ▼
┌─────────────────────────────────────────┐
│        Axios (HTTP Client)              │
└──────────────┬──────────────────────────┘
               │
               │ requests
               ▼
┌─────────────────────────────────────────┐
│       Backend API Server                │
└─────────────────────────────────────────┘

Nguyên tắc sử dụng

NÊN dùng Services khi:

  • Gọi API từ non-React code (utilities, event handlers, etc.)
  • Cần gọi API một lần rồi không cần tracking state
  • Không cần caching hay background refetch
  • Viết code không phụ thuộc React
// ✅ OK - utility function
export async function syncData() {
  const software = await appVersionService.getSoftwareList();
  const commands = await commandService.getCommandList();
  return { software, commands };
}

NÊN dùng Hooks khi:

  • Lấy/update dữ liệu trong React components
  • Cần caching và background refetch
  • Muốn dữ liệu tự động update
  • Cần tracking loading/error states
// ✅ OK - React component
function Dashboard() {
  const { data: software, isLoading } = useGetSoftwareList();
  const uploadMutation = useUploadSoftware();
  
  return <div>{/* ... */}</div>;
}

KHÔNG nên dùng Services khi:

  • Đang trong React component và cần state management
  • Cần automatic refetching
  • Cần auto-invalidation sau mutations
// ❌ WRONG
function Component() {
  const [data, setData] = useState([]);
  useEffect(() => {
    appVersionService.getSoftwareList().then(setData);
  }, []);
}

// ✅ RIGHT
function Component() {
  const { data } = useGetSoftwareList();
}

KHÔNG nên dùng Hooks khi:

  • Không phải trong React component
  • Không có React context
// ❌ WRONG - không thể gọi hooks ở đây
export function initApp() {
  const { data } = useGetSoftwareList(); // ERROR!
}

// ✅ RIGHT
export async function initApp() {
  const data = await appVersionService.getSoftwareList();
}

Summary

Services = Cơ sở API calls, có thể tái sử dụng ở bất kỳ đâu
Hooks = Lớp React trên services, tối ưu cho React components

Dùng Services khi bạn cần tính linh hoạt và độc lập với React
Dùng Hooks khi bạn muốn TanStack Query quản lý state và caching tự động