TanStack Query for React: Caching, Mutations, SSR

Introduction

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1283
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    980
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1029
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    552

Introduction

React developers constantly write boilerplate for data handling: useEffect, useState, reducers. Every new screen duplicates loading, caching, and updating logic. As a result, 60–70% of the code is boilerplate that distracts from business logic. On one project, we replaced Redux Thunk with TanStack Query, cut the code by 70%, and reduced bugs by a factor of three.

TanStack Query (formerly React Query) is a library for managing server state. It handles caching, background updates, request deduplication, pagination, mutations with optimistic updates, and prefetching. The result: data-related code shrinks by 60–70%, and bugs drop by 2–3 times. We have been configuring TanStack Query since its early days, with over 50 projects completed. We guarantee stable operation and 30–50% time savings. Contact us for a setup and get a complete data architecture in days.

Approach Boilerplate Complexity Scalability
Redux Thunk High High Medium
RTK Query Medium Medium High
TanStack Query Low Low High
SWR Low Low Medium

Problems TanStack Query Solves

Manual cache management: without the library, you have to manually synchronize requests, handle loading and errors. TanStack Query automatically caches data, deduplicates parallel requests, and updates the cache on mutations. Another pain point is cache invalidation. With hierarchical keys, you can invalidate all related lists with a single command without touching details.

How to Set Up QueryClient with Optimal Parameters

QueryClient is the central object that manages all queries. Its configuration affects performance. Here are recommended values:

Parameter Default Recommended Why
staleTime 0 5 * 60 * 1000 Data considered fresh for 5 minutes; no re-fetch on every mount
gcTime 5 * 60 * 1000 10 * 60 * 1000 Cache stored 10 minutes after removing references (formerly cacheTime)
retry 3 2 Retries on error – enough for transient failures
refetchOnWindowFocus false true Automatically update data when returning to the tab

Example basic initialization:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 10 * 60 * 1000, retry: 2, refetchOnWindowFocus: true, }, mutations: { retry: 0, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <Router /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ) } 

If the project uses Next.js App Router, we add server-side prefetch via dehydrate and HydrationBoundary.

Why Query Keys Are Key to Efficient Invalidation

Query keys are unique identifiers for a query. The key structure determines how the cache is invalidated after mutations. We use a hierarchical approach:

export const queryKeys = { products: { all: ['products'] as const, lists: () => [...queryKeys.products.all, 'list'] as const, list: (filters: ProductFilters) => [...queryKeys.products.lists(), filters] as const, details: () => [...queryKeys.products.all, 'detail'] as const, detail: (id: string) => [...queryKeys.products.details(), id] as const, }, users: { all: ['users'] as const, me: () => [...queryKeys.users.all, 'me'] as const, profile: (id: string) => [...queryKeys.users.all, 'profile', id] as const, }, } 

This allows invalidating all product lists with one command without touching details.

Main Hooks: useQuery, useMutation, useInfiniteQuery

The basic hook for fetching data is useQuery. It returns loading state, error, and data. For critical UI, use useSuspenseQuery with React Suspense.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' export function useProducts(filters: ProductFilters) { return useQuery({ queryKey: queryKeys.products.list(filters), queryFn: () => api.get<Product[]>('/products', { params: filters }), placeholderData: (prev) => prev, }) } export function useUpdateProduct() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateProductDto }) => api.patch<Product>(`/products/${id}`, data), onMutate: async ({ id, data }) => { await queryClient.cancelQueries({ queryKey: queryKeys.products.detail(id) }) const previous = queryClient.getQueryData<Product>(queryKeys.products.detail(id)) queryClient.setQueryData(queryKeys.products.detail(id), (old: Product) => ({ ...old, ...data })) return { previous } }, onError: (_, { id }, context) => { if (context?.previous) { queryClient.setQueryData(queryKeys.products.detail(id), context.previous) } }, onSettled: (_, __, { id }) => { queryClient.invalidateQueries({ queryKey: queryKeys.products.detail(id) }) }, }) } 

For infinite scroll – useInfiniteQuery:

export function useInfiniteProducts(filters: Omit<ProductFilters, 'page'>) { return useInfiniteQuery({ queryKey: queryKeys.products.list(filters), queryFn: ({ pageParam }) => api.get<PaginatedResponse<Product>>('/products', { params: { ...filters, page: pageParam, pageSize: 20 } }), initialPageParam: 1, getNextPageParam: (lastPage) => (lastPage.hasNextPage ? lastPage.page + 1 : undefined), }) } 

For simple pagination, use keepPreviousData in placeholderData.

How to Implement Optimistic Updates Without Pain

Optimistic update: the UI changes before the server responds. TanStack Query makes it easy: in onMutate update the cache, in onError roll back. The example above with useUpdateProduct shows this technique. The user sees instant feedback, and on error the data reverts to its original state.

Prefetching and SSR with Next.js

Prefetching loads data before navigating to a page. For example, on hover over a link:

function ProductLink({ id }: { id: string }) { const queryClient = useQueryClient() return ( <Link to={`/products/${id}`} onMouseEnter={() => { queryClient.prefetchQuery({ queryKey: queryKeys.products.detail(id), queryFn: () => api.get<Product>(`/products/${id}`), staleTime: 60_000, }) }} >Go</Link> ) } 

For SSR in Next.js App Router, use server-side prefetchQuery and HydrationBoundary:

import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' export default async function ProductsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: queryKeys.products.lists(), queryFn: () => fetchProductsServer(), }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <ProductList /> </HydrationBoundary> ) } 

More details in the official TanStack Query documentation.

What's Included in Our Service

We set up TanStack Query for your project:

  • Installation and QueryClient configuration tailored to your API.
  • Design of a hierarchical query keys structure for easy invalidation.
  • Writing custom hooks for all endpoints: useQuery, useMutation, useInfiniteQuery.
  • Implementation of optimistic updates for forms (likes, comments, editing).
  • Prefetching setup and integration with SSR/Next.js.
  • DevTools connection for debugging.
  • Documentation and team training.

Process:

  1. Analysis – audit of current stack and API, identification of integration points.
  2. Design – query keys scheme, caching strategy selection.
  3. Implementation – writing hooks, testing mutations, optimization.
  4. Testing – validation of invalidation, authorization, edge cases.
  5. Deployment – rollout, monitoring via DevTools.

Timeline: from 2 to 5 days depending on the number of endpoints. Cost is calculated individually. The savings on future support pay back the investment within weeks.

Contact us for a consultation – we'll help you implement TanStack Query and cut development time by 30–50%. Get a ready-made data architecture in days.