Master Data Fetching in React with TanStack Query: From Simple Queries to Optimistic Updates
This guide walks you through using TanStack Query in React, covering simple queries, custom hooks, selectors, dependent queries, pagination, infinite scrolling, query key factories, mutations, query invalidation, conditional fetching, optimistic updates, and global error handling with Suspense, all illustrated with clear code examples.
If you are still combining useEffect + fetch + useState, it’s time to switch to TanStack Query (also known as React Query). TanStack Query handles data fetching like an experienced backend specialist—it provides caching, retries, pagination, optimistic updates, and more, while keeping the UI smooth and the codebase tidy.
This article introduces TanStack Query’s features through real‑world use cases, code implementations, and explanations of why they matter.
https://tanstack.com
Install it first:
npm i @tanstack/react-query🔍 1. Simple Query
When to use: Fetch data on component mount—user info, lists, dashboard stats, etc.
const { data, isPending, isError } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});✅ Handles loading, error, caching, and refetching effortlessly.
🛠️ 2. Custom Query
When to use: Create reusable hooks such as useTodos() or useUser(id) to separate logic from UI.
function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
}✅ Components become cleaner, logic is reusable, and testability improves.
🎯 3. Selector
When to use: Derive data such as counts or filtered arrays.
const { data: activeCount } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.filter(t => !t.done).length,
});✅ Derived data updates without extra re‑renders.
🧩 4. Parameterized & Dependent Queries
When to use: APIs that require an ID or chained data.
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => fetchUserByEmail(email),
});
const userId = user?.id;
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPostsByUser(userId!),
enabled: !!userId,
});✅ Avoids waterfall loading and keeps dependent data synchronized.
📄 5. Pagination
When to use: Load data page by page—tables, galleries, search results, etc.
const { data, isFetching } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
placeholderData: (context) => context.default,
keepPreviousData: true,
});✅ No flicker between pages and automatic caching of previous pages.
∞️ 6. Infinite Query
When to use: Infinite scroll or “load more” scenarios.
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjectsCursor(pageParam),
getNextPageParam: (last) => last.nextCursor,
});✅ Seamless pagination with automatic appending of results.
🔑 7. Query Key Factory
When to use: Standardize query keys throughout the app.
const todosKeys = {
all: ['todos'],
lists: () => ['todos'],
detail: (id) => ['todos', id],
};✅ Makes invalidating, refetching, or debugging queries easier.
✍️ 8. Simple Mutation
When to use: Modify data—POST, PUT, DELETE.
const mutation = useMutation({
mutationFn: addTodoApi,
onSuccess: () => queryClient.invalidateQueries(['todos']),
});✅ Handles mutation state and integrates with the cache.
🔄 9. Invalidate Queries (Manual + Automatic)
When to use: Refresh data after a mutation.
onSuccess: () => queryClient.invalidateQueries(['todos'])Or use onSettled to invalidate regardless of success or failure.
✅ Keeps UI and server data in sync.
❌ 10. Disable Query
When to use: Conditionally fetch data—e.g., after login.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
enabled: isLoggedIn,
});✅ Prevents unnecessary requests and clarifies control flow.
⚡ 11. Optimistic Updates
UI‑first immediate updates:
onMutate: async (newTodo) => {
await queryClient.cancelQueries(['todos']);
const prev = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { prev };
};Cache‑only update:
queryClient.setQueryData(['user'], (old) => ({ ...old, name: 'Temp' }));✅ Users see instant feedback with safe rollback capability.
🌍 12. Global Error Handling & Suspense
Centralized error handling:
<QueryClientProvider client={queryClient}>
<ReactQueryErrorResetBoundary> … </ReactQueryErrorResetBoundary>
</QueryClientProvider>Suspense integration:
useQuery({
suspense: true,
queryKey: ['user'],
queryFn: fetchUser,
});✅ Cleaner handling of loading and error states across the whole application.
✅ Summary
TanStack Query gives you:
Structured server state
Lightning‑fast UX powered by caching
Automatic data synchronization
Excellent developer experience
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
