Skip to content
React rc state-management 5 min read

RTK Query

RTK Query is a powerful data fetching and caching layer that ships inside Redux Toolkit. It eliminates the hand-written thunks, loading flags, and cache bookkeeping that traditionally surround server state in Redux. You describe your API once with createApi, and RTK Query generates typed React hooks that handle requests, caching, deduplication, re-fetching, and invalidation for you. Think of it as a Redux-native alternative to TanStack Query that integrates directly with your existing store.

Why RTK Query exists

Most “global state” in a typical app is not really client state at all — it is server state: data that lives in a database and is fetched over the network. Server state is asynchronous, can go stale, and is shared across components. Managing it manually with useEffect and reducers leads to duplicated requests, scattered loading booleans, and brittle cache invalidation. RTK Query treats server data as a first-class cached resource so you can stop writing that boilerplate.

Defining an API with createApi

An API “slice” is created once. You give it a unique reducerPath, a baseQuery (usually fetchBaseQuery, a thin wrapper around fetch), and a set of endpoints.

// src/services/postsApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
  tagTypes: ['Post'],
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    getPost: builder.query({
      query: (id) => `posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }],
    }),
    addPost: builder.mutation({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: ['Post'],
    }),
  }),
});

export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = postsApi;

RTK Query auto-generates a hook for every endpoint, following the pattern use<EndpointName><Query|Mutation>. You never write these hooks yourself.

Wiring the API into the store

Add the generated reducer and the API middleware to your Redux store. The middleware powers caching, invalidation, and features like polling.

// src/store.js
import { configureStore } from '@reduxjs/toolkit';
import { postsApi } from './services/postsApi';

export const store = configureStore({
  reducer: {
    [postsApi.reducerPath]: postsApi.reducer,
  },
  middleware: (getDefault) => getDefault().concat(postsApi.middleware),
});

Wrap your app in the standard Redux <Provider store={store}> and you are ready to consume the hooks.

Queries and mutations in components

Query hooks return the cached data plus rich status flags. Mutation hooks return a trigger function and a result object.

import { useGetPostsQuery, useAddPostMutation } from './services/postsApi';

function Posts() {
  const { data: posts, isLoading, isError, refetch } = useGetPostsQuery();
  const [addPost, { isLoading: isSaving }] = useAddPostMutation();

  if (isLoading) return <p>Loading…</p>;
  if (isError) return <button onClick={refetch}>Retry</button>;

  return (
    <section>
      <button
        disabled={isSaving}
        onClick={() => addPost({ title: 'Hello', body: 'World', userId: 1 })}
      >
        {isSaving ? 'Saving…' : 'Add post'}
      </button>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </section>
  );
}

After addPost succeeds, the invalidatesTags: ['Post'] declaration tells RTK Query to mark the cached getPosts result as stale and automatically re-fetch it — no manual cache update required.

Caching and tag-based invalidation

RTK Query caches each query by its serialized arguments. Multiple components requesting the same data share one request and one cache entry. Cached data is kept for a configurable time after the last subscriber unmounts (keepUnusedDataFor, 60 seconds by default), then garbage-collected.

Invalidation is driven by tags. Queries declare what they provide with providesTags; mutations declare what they invalidate with invalidatesTags. When the tags overlap, the affected queries automatically re-fetch.

ConceptWhere it is setPurpose
tagTypescreateApiThe vocabulary of tags this API uses
providesTagsquery endpointLabels the cache entry this query fills
invalidatesTagsmutation endpointMarks matching cache entries stale
keepUnusedDataForendpoint or APISeconds to retain unused cache data

Tip: Use per-id tags like { type: 'Post', id } for granular invalidation. A mutation that edits one post can invalidate only that post instead of the entire list.

You can inspect the generated status flags returned by every query hook:

Output:

{
  data: [...],
  isLoading: false,   // true only on the first load
  isFetching: false,  // true on any in-flight request (incl. refetch)
  isSuccess: true,
  isError: false,
  refetch: fn
}

RTK Query vs TanStack Query

Both libraries solve server-state caching, but they target different ecosystems.

AspectRTK QueryTanStack Query
SetupEndpoints declared up front in createApiQuery functions defined inline per call
StoreLives inside the Redux storeStandalone, store-agnostic
HooksAuto-generated per endpointGeneric useQuery / useMutation
Cache keyEndpoint + serialized argsThe query key array you pass
Best fitApps already using Redux ToolkitApps without Redux, or wanting maximal flexibility

If your app already uses Redux Toolkit, RTK Query is the natural choice — it shares the store, devtools, and middleware. If you have no Redux and want the leanest server-cache layer, TanStack Query is excellent. They overlap heavily; pick one rather than running both.

Best Practices

  • Create a single API slice per backend and add endpoints to it, rather than many tiny createApi instances.
  • Use specific per-id tags so mutations invalidate only the entries they actually change.
  • Reach for the generated hooks (useGetPostsQuery) instead of dispatching thunks manually.
  • Keep server state in RTK Query and client-only UI state in regular Redux slices or local state.
  • Prefer isFetching for background refresh spinners and isLoading for the very first load.
  • Use transformResponse to normalize payloads at the endpoint boundary instead of in components.
  • Enable polling or refetchOnFocus only where freshness genuinely matters, to avoid needless requests.
Last updated June 14, 2026
Was this helpful?