TanStack Query Best Practices
Server state management with automatic caching and synchronization.
Instructions
- Setup
// providers/QueryProvider.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime) retry: 1, refetchOnWindowFocus: false, }, }, });
export function QueryProvider({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }
- Query Keys Factory
// lib/queryKeys.ts export const queryKeys = { users: { all: ['users'] as const, list: (filters: UserFilters) => [...queryKeys.users.all, 'list', filters] as const, detail: (id: string) => [...queryKeys.users.all, 'detail', id] as const, }, posts: { all: ['posts'] as const, list: (filters: PostFilters) => [...queryKeys.posts.all, 'list', filters] as const, detail: (id: string) => [...queryKeys.posts.all, 'detail', id] as const, byUser: (userId: string) => [...queryKeys.posts.all, 'user', userId] as const, }, } as const;
- Custom Query Hook
// hooks/useUsers.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '@/lib/queryKeys'; import { userApi } from '@/lib/api';
export function useUsers(filters?: UserFilters) { return useQuery({ queryKey: queryKeys.users.list(filters ?? {}), queryFn: () => userApi.getAll(filters), }); }
export function useUser(id: string) { return useQuery({ queryKey: queryKeys.users.detail(id), queryFn: () => userApi.getById(id), enabled: !!id, // Only run when id exists }); }
- Mutations
// hooks/useCreateUser.ts export function useCreateUser() { const queryClient = useQueryClient();
return useMutation({ mutationFn: (data: CreateUserDto) => userApi.create(data), onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: queryKeys.users.all }); }, onError: (error) => { toast.error(error.message); }, }); }
// Usage function CreateUserForm() { const { mutate, isPending } = useCreateUser();
const handleSubmit = (data: CreateUserDto) => { mutate(data, { onSuccess: () => toast.success('User created!'), }); }; }
- Optimistic Updates
export function useUpdateUser() { const queryClient = useQueryClient();
return useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) => userApi.update(id, data), onMutate: async ({ id, data }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: queryKeys.users.detail(id) });
// Snapshot previous value
const previousUser = queryClient.getQueryData(queryKeys.users.detail(id));
// Optimistically update
queryClient.setQueryData(queryKeys.users.detail(id), (old: User) => ({
...old,
...data,
}));
return { previousUser };
},
onError: (err, { id }, context) => {
// Rollback on error
queryClient.setQueryData(
queryKeys.users.detail(id),
context?.previousUser
);
},
onSettled: (_, __, { id }) => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
},
}); }
- Infinite Query (Pagination)
export function useInfinitePosts() { return useInfiniteQuery({ queryKey: queryKeys.posts.all, queryFn: ({ pageParam = 1 }) => postApi.getAll({ page: pageParam }), getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, initialPageParam: 1, }); }
// Usage function PostList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfinitePosts();
return ( <> {data?.pages.map((page) => page.posts.map((post) => <PostCard key={post.id} post={post} />) )} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}> {isFetchingNextPage ? 'Loading...' : 'Load More'} </button> )} </> ); }
- Prefetching
// Prefetch on hover function UserLink({ userId }: { userId: string }) { const queryClient = useQueryClient();
const prefetchUser = () => { queryClient.prefetchQuery({ queryKey: queryKeys.users.detail(userId), queryFn: () => userApi.getById(userId), staleTime: 1000 * 60 * 5, }); };
return (
<Link href={/users/${userId}} onMouseEnter={prefetchUser}>
View User
</Link>
);
}
- Dependent Queries
function useUserPosts(userId: string | undefined) { const userQuery = useUser(userId!);
return useQuery({ queryKey: queryKeys.posts.byUser(userId!), queryFn: () => postApi.getByUser(userId!), enabled: !!userId && userQuery.isSuccess, }); }
Common Patterns
Pattern Usage
staleTime
How long data stays fresh
gcTime
How long unused data stays in cache
enabled
Conditional fetching
select
Transform response data
placeholderData
Show while loading
References
-
TanStack Query Docs
-
Practical React Query