Skip to content
ExponaBLOG
REACT & NEXT.JS · DAY 017 / 100

RTK Query vs TanStack Query: what developers should know

Both are excellent. Choose based on your stack.

Expona Admin7 min read

RTK Query vs TanStack Query

After 5+ years shipping production platforms, I've reached for both RTK Query and TanStack Query countless times, and the honest answer is: they're both excellent, but they serve different team contexts. The choice between RTK Query vs TanStack Query isn't about which is objectively "better"—it's about whether you're already invested in Redux, how much boilerplate you want to maintain, and whether you need server state and client state to live in the same place.

The Core Tradeoff: Redux Ecosystem vs Standalone Library

RTK Query is built on top of Redux Toolkit. TanStack Query (formerly React Query) is completely standalone and brings its own caching layer. This fundamental difference cascades into everything else.

If your codebase already has Redux slices managing authentication, user preferences, or other client state, RTK Query feels like the natural extension. It lives inside your Redux store, shares your action dispatch patterns, and hooks into Redux DevTools. I've found this particularly valuable on larger teams where developers already know Redux Thunk, middleware, and store structure.

If you're building a new project or a smaller team, TanStack Query's zero-Redux requirement is liberating. You don't need to maintain action creators, reducers, or middleware just to fetch data. Your queries are your source of truth.

Already Using Redux? RTK Query Fits Naturally

When I started working on a travel booking platform two years ago, the existing team had a mature Redux architecture for managing search filters, user booking history, and payment state. We chose RTK Query to handle the API layer, and it integrated seamlessly.

Here's a minimal example of how RTK Query slots into an existing Redux setup:

tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const travelApi = createApi({
  reducerPath: 'travelApi',
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.example.com',
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  endpoints: (builder) => ({
    searchFlights: builder.query({
      query: (params: SearchParams) => ({
        url: '/flights/search',
        params,
      }),
      // RTK Query caches by endpoint + parameters automatically
    }),
    bookFlight: builder.mutation({
      query: (booking: BookingPayload) => ({
        url: '/flights/book',
        method: 'POST',
        body: booking,
      }),
      // Invalidate search results after booking
      invalidatesTags: ['Flights'],
    }),
  }),
});

export const { useSearchFlightsQuery, useBookFlightMutation } = travelApi;

Your Redux store config includes RTK Query's reducer:

tsx
import { configureStore } from '@reduxjs/toolkit';
import { travelApi } from './apis/travelApi';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    filters: filterReducer,
    [travelApi.reducerPath]: travelApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(travelApi.middleware),
});

export type RootState = ReturnType;

Now your API state and client state live together in Redux DevTools. You can subscribe to loading states, errors, and cached data using Redux selectors. For teams already comfortable with Redux patterns, this feels native, not bolted-on.

No Redux? TanStack Query Is Lighter

I built a ride-hailing dashboard for a smaller team that explicitly wanted to avoid Redux. The requirements were straightforward: fetch driver locations, handle real-time position updates, and invalidate caches when assignments change. TanStack Query was a perfect fit.

Here's how the same flight search looks in TanStack Query:

tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export const useSearchFlights = (params: SearchParams) => {
  return useQuery({
    queryKey: ['flights', 'search', params],
    queryFn: async () => {
      const response = await fetch(
        `https://api.example.com/flights/search?${new URLSearchParams(params)}`
      );
      if (!response.ok) throw new Error('Search failed');
      return response.json();
    },
    staleTime: 1000 * 60 * 5, // 5 minutes
  });
};

export const useBookFlight = () => {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (booking: BookingPayload) => {
      const response = await fetch('https://api.example.com/flights/book', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(booking),
      });
      if (!response.ok) throw new Error('Booking failed');
      return response.json();
    },
    onSuccess: () => {
      // Invalidate the search query to refetch fresh results
      queryClient.invalidateQueries({ queryKey: ['flights', 'search'] });
    },
  });
};

Notice: no Redux store, no action creators, no reducer boilerplate. TanStack Query manages its own in-memory cache keyed by `queryKey`. You compose hooks directly in your components. The API is minimal and declarative.

Both Cache, Dedupe, Refetch and Invalidate

This is where they converge. Both libraries solve the same core problems: caching API responses, deduplicating in-flight requests, and invalidating stale data.

Caching: Both store responses in memory and serve them on subsequent identical requests. RTK Query uses a normalized cache; TanStack Query uses the query key as a cache identity.

Deduplication: If two components call the same query simultaneously, both libraries make only one network request and share the response. On a CRM I maintained, when a user opened the customer detail page, three components tried to fetch the same customer record. RTK Query and TanStack Query both recognized the duplicate and fired a single request.

Refetching: Both support `staleTime` (when data becomes stale) and `gcTime` (formerly `cacheTime`, when unused data is garbage collected). You can also manually trigger refetches:

RTK Query:

tsx
const { data, refetch } = useSearchFlightsQuery(params);

// Manual refetch
<button onClick={() => refetch()}>Refresh</button>

TanStack Query:

tsx
const { data, refetch } = useSearchFlights(params);

<button onClick={() => refetch()}>Refresh</button>

Invalidation: Both support declarative cache invalidation to mark data as stale after a mutation:

RTK Query uses `invalidatesTags` (above). TanStack Query uses `queryClient.invalidateQueries()`. Both force a refetch on the next access.

Generate Endpoints from OpenAPI with Either

On a SaaS platform with a large API surface, manually writing every query and mutation felt inefficient. Both RTK Query and TanStack Query have tooling to generate boilerplate from OpenAPI specs.

RTK Query: Use the `@rtk-incubator/rtk-query-codegen-openapi` package:

tsx
// openapi-config.ts
import { generateApi } from '@rtk-incubator/rtk-query-codegen-openapi';
import type { ConfigFile } from '@rtk-incubator/rtk-query-codegen-openapi';

const config: ConfigFile = {
  schemaFile: 'https://api.example.com/openapi.json',
  apiFile: './src/apis/baseApi.ts',
  apiImport: 'baseApi',
  outputFile: './src/apis/generatedApi.ts',
  exportName: 'generatedApi',
  hooks: true,
};

export default config;

Run the codegen, and you get fully typed RTK Query hooks for every endpoint in your API spec.

TanStack Query: Tools like `openapi-ts` and `@hey-api/openapi-ts` generate type-safe fetcher functions, which you then wrap with TanStack Query hooks. It's slightly more manual but integrates cleanly with TanStack Query's patterns.

Either approach eliminates manual type definitions and keeps your data layer synchronized with your API schema.

Common Mistakes

Mistake 1: Treating server state like client state

I've seen teams use RTK Query to store user preferences (client state) and API responses (server state) in the same slice. This creates confusion. RTK Query and TanStack Query are purpose-built for server state. Keep client state separate—use a context, Zustand, or plain Redux for UI toggles, form state, or theme preferences.

Mistake 2: Not setting staleTime appropriately

By default, RTK Query treats data as stale immediately; TanStack Query defaults to 0 staleTime. On a trading platform, I forgot to set `staleTime` for price quotes, causing unnecessary refetches every 30 seconds. Define staleTime based on how fast your data actually changes. Exchange rates? 30 seconds. User profile? 5 minutes.

Mistake 3: Over-normalizing query keys or tags

RTK Query's `providedTags` and `invalidatesTags` are powerful but easy to misuse. I once provided `['User', { type: 'User', id: userId }]` for every user query, then invalidated the entire `'User'` tag after a global settings update. This busted all user caches unnecessarily. Invalidate granularly.

Mistake 4: Ignoring request deduplication

Both libraries dedupe in-flight requests, but only if the cache key or endpoint parameters match exactly. On a form with autocomplete, I fired a new request for every keystroke without debouncing, assuming dedup would save me. Dedup doesn't prevent the request; it just prevents duplicate concurrent requests. Always debounce expensive queries client-side.

Key Takeaways

  • Already deep in Redux? RTK Query is your answer. It integrates with Redux store, DevTools, and existing action patterns.
  • Starting fresh or avoiding Redux? TanStack Query is lighter, easier to learn, and has a gentler onboarding curve.
  • Both cache, dedupe, refetch, and invalidate server state. The core data-fetching capabilities are comparable.
  • Both support OpenAPI codegen. Use it to stay synchronized with your API spec and eliminate manual type definitions.
  • Avoid common pitfalls: don't mix server state and client state, set staleTime based on your domain, invalidate granularly, and debounce expensive queries.
  • Neither is objectively "better." Choose based on your stack, team familiarity, and codebase patterns. Switching between them is possible but adds friction.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles