Custom query
Tạo custom query để tái sử dụng một query cho nhiều component:
/* queries/contacts.ts */
export const useGetContacts = (searchParams: GetContactsSearchParams) => useQuery({
queryKey: ["contacts", "list", { searchParams }],
queryFn: () => getContacts(searchParams),
});
/* components/contacts.tsx */
const { data } = useGetContacts(searchParams);
Trong cách viết trên, các option của query là cố định. Trong trường hợp tôi muốn linh hoạt thay đổi các option tùy vào bối cảnh sử dụng, tôi sẽ dùng hàm queryOptions để tạo nhóm các option chung mà vẫn giữ được khả năng type inference của useQuery. Ví dụ tôi chỉ lấy tổng bản ghi thỏa mãn điều kiện search mà không cần lấy list data:
/* queries/contacts.ts */
export const getContactsQueryOptions = (searchParams: GetContactsSearchParams) => queryOptions({
queryKey: ["contacts", "list", { searchParams }],
queryFn: () => getContacts(searchParams),
});
/* components/contacts-overview.tsx */
const { data: total } = useQuery({
...getContactsQueryOptions(searchParams),
select: (data) => data.total
});
// dĩ nhiên bạn dùng cách này cũng được:
// const { data: { total } } = useGetContacts(searchParams);
Prefetching
Bạn có thể dùng prefetchQuery để fetch trước dữ liệu nào đó vào cache để có trải nghiệm load trang tiếp theo mượt mà. Ví dụ trong một màn hình list có phân trang, tôi đang ở trang 1, tôi có thể fetch trước data cho trang 2:
/* components/contacts.tsx */
const queryClient = useQueryClient();
useEffect(() => {
queryClient.prefetchQuery(getContactsQueryOptions({ ...searchParams, page: searchParams.page + 1 }));
}, [queryClient, searchParams.page]);
Query key factories
Chúng ta nên hằng số hóa các query key để đảm bảo sự nhất quán trong dự án, tránh để lạm phát query key hoặc invalidate nhầm key. Bạn có thể dùng hằng số, hoặc viết hàm trả về query key theo các trường hợp cụ thể, sau này gọi cho dễ, nhất là khi bạn cần invalidate chính xác query key.
/* queries/contacts.ts */
export const queryKeys = {
all: () => ["contacts"],
getContacts: (searchParams: GetContactsSearchParams) => [
...queryKeys.all(),
"list",
{ searchParams },
],
getContact: (contactId: string) => [
...queryKeys.all(),
"one",
{ contactId },
],
};
export const useGetContacts = (searchParams: GetContactsSearchParams) => useQuery({
queryKey: queryKeys.getContacts(searchParams),
queryFn: () => getContacts(searchParams),
});
Automatic query invalidation
Cách sử dụng useMutation và invalidate query cơ bản như sau:
/* queries/contacts.ts */
export const useDeleteContact = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (contactId: string) => deleteContact(contactId),
onSettled: () => queryClient.invalidateQueries({ queryKey: queryKeys.all() }),
});
}
useQueryClient bị lặp lại hơi nhiều. Chúng ta có thể tối ưu hơn bằng cách đưa xử lý invalidate vào instance toàn cục, chỉ cần truyền query key qua option meta:
/* src/components/providers/query-provider.tsx */
const queryClient = new QueryClient({
onSettled: (data, error, variables, context, mutation) => {
if (mutation.meta?.invalidatesQuery) {
queryClient.invalidateQueries({ queryKey: mutation.meta.invalidatesQuery });
}
},
});
// const TanstackQueryProvider: React.FC<React.PropsWithChildren> = (children) => {
// return (
// <QueryClientProvider client={queryClient}>
// {children}
// </QueryClientProvider>
// )
// }
// export { TanstackQueryProvider as QueryProvider }
/* queries/contacts.ts */
export const getDeleteContactMutationOptions = mutationOptions({
mutationFn: (contactId: string) => deleteContact(contactId),
meta: { invalidatesQuery: queryKeys.all() }
});
Suspense queries
Thông thường, chúng ta sẽ lấy trạng thái của state isLoading để render skeleton UI trong khi chờ data được fetch về hoàn tất.
export const ContactsTable: React.FC = () => {
const { data, isLoading } = useQuery(...);
if (isLoading) return <LoadingSkeleton />;
// ...
}
Chúng ta có thể thay bằng useSuspenseQuery và render skeleton UI bên ngoài component bằng Suspense:
/* app/contacts/_components/contacts-table.tsx */
const { data } = useSuspenseQuery(...);
/* app/contacts/page.tsx */
return (
<Suspense fallback={<LoadingSkeleton />}>
<ContactsTable />
</Suspense>
)