React-query
TanStack Query (React Query)
TanStack Query (React Query) Setup & Queries import { QueryClient, QueryClientProvider, useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; …
TanStack Query (React Query)
Setup & Queries
import { QueryClient, QueryClientProvider, useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// Setup — wrap app in provider
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 min — data considered fresh
gcTime: 10 * 60 * 1000, // 10 min — keep inactive queries in cache
retry: 2,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// useQuery — fetch & cache data
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['users', userId], // cache key — array, be specific
queryFn: () => api.get(`/users/${userId}`),
enabled: !!userId, // only run when userId exists
staleTime: 60_000, // 1 min fresh
select: (data) => data.profile, // transform data before return
placeholderData: keepPreviousData, // keep old data while fetching new
});
if (isLoading) return <Spinner />;
if (isError) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
// Query with dependencies
const { data: orders } = useQuery({
queryKey: ['orders', userId, { status: 'pending' }],
queryFn: () => api.get('/orders', { params: { userId, status: 'pending' } }),
enabled: !!userId,
});Mutations & Cache Updates
// useMutation
function CreatePost() {
const queryClient = useQueryClient();
const { mutate, mutateAsync, isPending, isError } = useMutation({
mutationFn: (data: CreatePostInput) => api.post('/posts', data),
// Optimistic update
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const prev = queryClient.getQueryData<Post[]>(['posts']);
queryClient.setQueryData<Post[]>(['posts'], old =>
old ? [{ ...newPost, id: 'temp' }, ...old] : old
);
return { prev }; // context passed to onError
},
onError: (_err, _vars, context) => {
if (context?.prev) queryClient.setQueryData(['posts'], context.prev);
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
// or update cache directly:
queryClient.setQueryData(['posts', data.id], data);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return <button onClick={() => mutate({ title: 'New Post' })}>Create</button>;
}
// Prefetch
await queryClient.prefetchQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
});
// Invalidate & refetch
queryClient.invalidateQueries({ queryKey: ['posts'] }); // all posts
queryClient.invalidateQueries({ queryKey: ['posts', userId] }); // specific
queryClient.removeQueries({ queryKey: ['posts'] }); // remove from cache
queryClient.resetQueries({ queryKey: ['posts'] }); // reset to initialInfinite Queries & Advanced
// Infinite scroll
function PostFeed() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) =>
api.get('/posts', { params: { cursor: pageParam, limit: 20 } }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
const posts = data?.pages.flatMap(page => page.posts) ?? [];
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
</div>
);
}
// useQueries — parallel queries
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
});
// Query keys factory pattern (recommended)
const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
};
// Invalidate all post queries: queryClient.invalidateQueries({ queryKey: postKeys.all })