RTK Query

4 min read source

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.

TL;DR

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.

Interview Q&A

Q: What does a basic API slice look like?

A:

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",
      providesTags: ["Post"],                       // this data is tagged "Post"
    }),
    addPost: build.mutation<Post, Partial<Post>>({
      query: (body) => ({ url: "posts", method: "POST", body }),
      invalidatesTags: ["Post"],                    // invalidate → getPosts refetches
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = api;

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

Q: How do the generated hooks behave?

A:

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.

Q: How does tag-based invalidation work?

A: 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:

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.

Q: How do you do polling and optimistic updates?

A: Polling is a hook option:

useGetPostsQuery(undefined, { pollingInterval: 5000 });

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

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 ../../11_apis_data_fetching/03_optimistic_updates.md.

Q: RTK Query vs TanStack Query — how do you choose?

A:

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.

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

A: 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 / edge cases

  • 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.

What a senior is expected to say

  • “RTK Query is Redux’s built-in server-state cache: createApi endpoints generate hooks; the cache is keyed by endpoint+args; tags drive invalidation.”
  • “providesTags/invalidatesTags is declarative invalidation — tag by id for granularity, or you refetch everything.”
  • “Optimistic updates use onQueryStarted + updateQueryData with patch.undo() on failure.”
  • “I pick RTK Query when already on Redux, TanStack Query otherwise — I wouldn’t add Redux just for it.”

Cross-references

Further reading