Một useFetch gọn gàng chỉ giải quyết phần dễ nhất: gọi API rồi set state. Phần khó nằm ở những thứ đến sau khi tính năng lớn lên:
- Race condition: hai request cùng key về không đúng thứ tự, kết quả cũ ghi đè kết quả mới. Phải huỷ hoặc đánh dấu bỏ qua ở cleanup.
- Cache và chia sẻ giữa component: hai màn hình cùng cần một dữ liệu sẽ gọi API hai lần nếu không có store chung.
- Dữ liệu cũ sau mutation: sau khi tạo/sửa phải làm mới đúng những query liên quan.
- Retry, dedupe, refetch khi focus lại tab, phân trang, tải trước.
Các thư viện này gói sẵn toàn bộ phần trên và đặt cache ra ngoài component, nên state không chết theo unmount.
jsx
const { data, isPending, error } = useQuery({
queryKey: ['orders', page],
queryFn: () => fetchOrders(page),
})Khi nào tự viết vẫn hợp lý: app nhỏ, vài endpoint, không cần cache dùng chung — thêm một dependency lớn không đáng.
Với App Router, phần lớn dữ liệu đọc nên fetch ở Server Component, thư viện client chỉ dùng cho dữ liệu thay đổi liên tục hoặc phụ thuộc tương tác.