RTK Query

Updated 4 interview angles 4 min read source
On this page10
  1. What does a basic API slice look like?
  2. How do the generated hooks behave?
  3. How does tag-based invalidation work?
  4. How do you do polling and optimistic updates?
  5. RTK Query vs TanStack Query — how do you choose?
  6. When do you still need regular Redux slices alongside RTK Query?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

RTK Query

RTK Query is the data-fetching and caching layer built into Redux Toolkit. It’s the Redux ecosystem’s answer to TanStack Query: define your API once, get auto-generated hooks with caching, dedup, invalidation, polling, and optimistic updates — no hand-written thunks, loading flags, or reducers for server data.

createApi defines endpoints (query for reads, mutation for writes). It generates hooks (useGetPostsQuery, useAddPostMutation) and a reducer/middleware you add to the store. The cache is keyed by endpoint + serialized args; tags drive invalidation (a mutation invalidates tags that queries provide, triggering refetch). Pick RTK Query when you’re already on Redux; pick TanStack Query if you’re not.

What does a basic API slice look like?

ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: ["Post"],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => "posts",
      // this data is tagged "Post"
      providesTags: ["Post"],
    }),
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({ url: "posts", method: "POST", body }),
      // invalidate → getPosts refetches
      invalidatesTags: ["Post"],
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = api;

Add api.reducer under api.reducerPath and api.middleware in configureStore, and you’re done.

How do the generated hooks behave?

tsx
const { data, isLoading, isFetching, error, refetch } = useGetPostsQuery();
const [addPost, { isLoading: adding }] = useAddPostMutation();
  • Mount → fetch (or serve cache). Two components calling useGetPostsQuery() share one request (dedup).
  • isLoading = first load; isFetching = any in-flight fetch (including background refetch).
  • Data is cached by (endpoint, args). When the last subscriber unmounts, it’s kept for keepUnusedDataFor (default 60s) then garbage-collected.

How does tag-based invalidation work?

Queries declare what they provide, mutations declare what they invalidate. RTK Query refetches any active query whose provided tags were invalidated. For per-item granularity, tag by id:

ts
getPosts: build.query({
  query: () => "posts",
  providesTags: (result) =>
    result ? [...result.map((p) => ({ type: "Post" as const, id: p.id })), { type: "Post", id: "LIST" }]
           : [{ type: "Post", id: "LIST" }],
}),
updatePost: build.mutation({
  query: (p) => ({ url: `posts/${p.id}`, method: "PUT", body: p }),
  invalidatesTags: (r, e, p) => [{ type: "Post", id: p.id }],   // refetch just that post
}),

This is the mental model to nail in interviews: providesTags + invalidatesTags = declarative cache invalidation, the RTK analog of TanStack Query’s queryClient.invalidateQueries.

How do you do polling and optimistic updates?

Polling is a hook option:

tsx
useGetPostsQuery(undefined, { pollingInterval: 5000 });

Optimistic update via onQueryStarted — patch the cache immediately, undo on failure:

ts
updatePost: build.mutation({
  query: (p) => ({ url: `posts/${p.id}`, method: "PUT", body: p }),
  async onQueryStarted(p, { dispatch, queryFulfilled }) {
    const patch = dispatch(api.util.updateQueryData("getPosts", undefined,
      (draft) => { const x = draft.find((d) => d.id === p.id); if (x) Object.assign(x, p); }));
    try { await queryFulfilled; } catch { patch.undo(); }   // rollback on error
  },
}),

updateQueryData uses Immer so you “mutate” the draft; patch.undo() reverts. Same pattern, library-specific API — contrast TanStack’s onMutate/onError in Optimistic Updates and Rollback.

RTK Query vs TanStack Query — how do you choose?

RTK Query TanStack Query
Ecosystem built into Redux Toolkit standalone, framework-agnostic
Cache location the Redux store (one devtools) its own cache
API style declarative endpoints + tags imperative queryKey + queryFn
Best when already using Redux for client state no other reason to run Redux

Both solve server state well. The deciding factor is usually “are we already on Redux?” Don’t add Redux just to get RTK Query — TanStack Query needs no store.

When do you still need regular Redux slices alongside RTK Query?

For genuine client state (UI, drafts, cross-cutting flags). RTK Query handles server data; a normal createSlice handles the local stuff. They coexist in the same store.

Gotchas

  • Forgetting to add the middleware — caching, invalidation, and polling silently don’t work without api.middleware in configureStore.
  • Over-broad tags — invalidating "Post" refetches every post list/detail; tag by id for surgical invalidation.
  • isLoading vs isFetching — show skeletons on isLoading, a subtle spinner on background isFetching; conflating them flickers the UI.
  • keepUnusedDataFor too low evicts cache aggressively (refetch on every revisit); too high serves stale data — tune per endpoint.
  • fetchBaseQuery is minimal — for auth refresh/retry, wrap it with a custom baseQuery (baseQueryWithReauth).
  • It’s still server state — don’t copy RTK Query results into a slice; subscribe to the query.

Further reading

Interview angle 4

  • “What is RTK Query?” - Redux’s built-in server-state cache. You declare endpoints with createApi, it generates the hooks, and the cache is keyed by endpoint plus serialised arguments - so the same query from two components is one request.
  • “How does invalidation work?” - declaratively, through tags. A query providesTags, a mutation invalidatesTags, and matching tags trigger a refetch. Tag by id where you can: tagging the whole collection means every mutation refetches every list.
  • “How do you do an optimistic update?” - onQueryStarted with updateQueryData to patch the cache immediately, and patch.undo() in the catch when the request fails. The undo handle is the part people forget, and without it a failed mutation leaves the UI lying.
  • “RTK Query or TanStack Query?” - RTK Query if the app is already on Redux, since it reuses the store and devtools you have. TanStack Query otherwise - adding Redux solely to get RTK Query is the wrong way round.