TanStack Query adalah solusi standar untuk manajemen status server dalam aplikasi React.
useQuery: Mengambil dan Menyimpan Data
useQuery berlangganan status server dengan queryKey sebagai kunci cache.
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Setup: wrap your app
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Basic query
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery<User>({
queryKey: ['user', userId], // cache key — must be unique
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: userId > 0, // only run if userId is valid
staleTime: 1000 * 60 * 10, // override global default
});
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return <div>{data.name} — {data.email}</div>;
}useMutation: Membuat, Memperbarui, Menghapus
useMutation menangani operasi CRUD dengan callback onSuccess.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreatePostInput {
title: string;
body: string;
userId: number;
}
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreatePostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onSuccess: (newPost) => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or add directly to cache (no refetch needed)
queryClient.setQueryData(['post', newPost.id], newPost);
},
onError: (error) => {
console.error('Failed to create post:', error);
},
});
}
function CreatePostForm() {
const mutation = useCreatePost();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
</form>
);
}Pembaruan Optimistis untuk UI Instan
Pembaruan optimistis menerapkan perubahan sebelum respons server.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
function useToggleTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (todo: Todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !todo.completed }),
}).then(r => r.json()),
// Optimistic update — runs BEFORE the mutation
onMutate: async (updatedTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot current data for rollback
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// Optimistically update the cache
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo =>
todo.id === updatedTodo.id
? { ...todo, completed: !todo.completed }
: todo
)
);
return { previousTodos }; // context for onError
},
// If mutation fails, roll back to snapshot
onError: (_err, _variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}Kueri Tak Terbatas untuk Paginasi
useInfiniteQuery menangani paginasi berbasis kursor.
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Post[];
nextCursor?: string;
}
function useInfinitePosts() {
return useInfiniteQuery<Page>({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}`
: '/api/posts';
return fetch(url).then(r => r.json());
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<div>
{data?.pages.flatMap(page => page.items).map(post => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}Pola Kunci Kueri dan Prefetching
Factory kunci yang konsisten mencegah ketidakcocokan cache.
// Query key patterns — factory functions for consistency
const queryKeys = {
all: ['posts'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
};
// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });
// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });
// Prefetching for better UX
async function prefetchUser(userId: number) {
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
});
}
// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
View Profile
</link>TanStack Query vs Solusi Lain
| Feature | TanStack Query | Redux Toolkit | SWR | Apollo |
|---|---|---|---|---|
| Caching | Built-in (staleTime, gcTime) | Manual | Built-in | Built-in (Apollo-only) |
| Optimistic updates | First-class | Manual | Basic | First-class |
| Infinite query | useInfiniteQuery | Manual | useSWRInfinite | fetchMore |
| Mutations | useMutation | RTK Query | No (use fetch) | useMutation |
| DevTools | Excellent | Excellent | Basic | Good |
| REST + GraphQL | Both | Both | REST focused | GraphQL only |
Praktik Terbaik
- Gunakan factory kunci kueri untuk konsistensi.
- Setel staleTime berdasarkan persyaratan kesegaran data.
- Selalu sediakan status loading dan error.
- Gunakan pembaruan optimistis untuk tindakan cepat.
- Prefetch data saat hover atau transisi rute.
FAQ
Perbedaan antara staleTime dan gcTime?
staleTime mengontrol kapan data dianggap basi, gcTime berapa lama tersimpan di memori.
Bagaimana TanStack Query menangani race conditions?
Secara otomatis mendeduplikasi permintaan yang identik.
TanStack Query dengan Redux atau Zustand?
Komplementer: TanStack Query untuk status server, Zustand untuk status klien.
Menangani kesalahan autentikasi secara global?
Gunakan callback onError global dari QueryClient.
Perubahan di TanStack Query v5?
cacheTime diubah nama menjadi gcTime, parameter queryFn sebagai objek, dukungan RSC.