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.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Master Data Fetching in React with TanStack Query: From Simple Queries to Optimistic Updates

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CacheReactpaginationData FetchingTanStack QueryOptimistic Update
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.