What is RTK Query and how does it differ from Axios or fetch?
RTK Query is Redux Toolkit's built-in data-fetching + caching layer. axios/fetch are transport — you still write loading/error state, cache, dedup, refetch logic yourself. RTKQ generates Redux slices + React hooks from endpoint definitions: free caching, automatic refetch, tag-based invalidation, request dedup, polling, optimistic updates, normalized cache. Use RTKQ if you're already on Redux; otherwise React Query/SWR are equivalent libraries with the same model and no Redux dependency.
RTK Query (RTKQ) is the data-fetching/caching layer bundled with Redux Toolkit. It sits on top of fetch/axios — you can use either as the underlying transport — and adds everything you'd normally hand-roll: cache, dedup, refetch, loading state, invalidation, polling, optimistic updates.
axios and fetch are just HTTP clients. They give you bytes; everything else (state, cache, race conditions, retries) is on you.
What you write with fetch alone
function User({ id }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const ctrl = new AbortController();
setLoading(true);
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then(setUser)
.catch(err => err.name !== 'AbortError' && setError(err))
.finally(() => setLoading(false));
return () => ctrl.abort();
}, [id]);
if (loading) return <Spinner />;
if (error) return <Error err={error} />;
return <Profile user={user} />;
}And then you do that on every page. With no cache. With no dedup. With no refetch-on-window-focus. Two components requesting /users/42 issue two requests.
What you write with RTKQ
// api.ts — one definition for the whole app
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (build) => ({
getUser: build.query<User, string>({
query: (id) => `users/${id}`,
providesTags: (_r, _e, id) => [{ type: 'User', id }],
}),
updateUser: build.mutation<User, Partial<User> & { id: string }>({
query: ({ id, ...patch }) => ({ url: `users/${id}`, method: 'PATCH', body: patch }),
invalidatesTags: (_r, _e, { id }) => [{ type: 'User', id }],
}),
}),
});
export const { useGetUserQuery, useUpdateUserMutation } = api;function User({ id }) {
const { data: user, isLoading, error } = useGetUserQuery(id);
if (isLoading) return <Spinner />;
if (error) return <Error err={error} />;
return <Profile user={user} />;
}You get for free:
- Cache by query key. Two components asking for
getUser(42)issue one request and share the result. - Dedup of in-flight requests with the same key.
- Refetch on mount, focus, reconnect (configurable).
- Polling (
useGetUserQuery(id, { pollingInterval: 5000 })). - Invalidation by tags —
updateUserinvalidates['User', id], which forcesgetUser(id)to refetch. - Loading + error state in the hook return.
- Optimistic updates via
onQueryStarted. - Code generation from OpenAPI / GraphQL schemas.
Comparison
| Concern | fetch / axios | RTK Query |
|---|---|---|
| Transport | yes | uses fetch (or any baseQuery) |
| Loading/error state | manual useState | hook returns it |
| Cache | none | normalized cache, keyed by args |
| Dedup | none | automatic |
| Refetch on focus | manual | one flag |
| Invalidation | manual (refetch by hand) | tag-based |
| Polling | setInterval + cleanup | pollingInterval option |
| Optimistic updates | manual + rollback | onQueryStarted |
| Redux dependency | no | yes |
| Bundle | tiny (axios ~15KB) | ~9KB on top of RTK |
When to use RTKQ
- You're already using Redux Toolkit.
- You want a single canonical place for endpoint definitions.
- You want tag-based invalidation (good for "I changed X, refetch everything depending on X").
- You want generated hooks from OpenAPI.
When not to use RTKQ
- You don't want Redux. React Query and SWR offer the same model (cache, dedup, refetch, invalidation, mutations) without any Redux dependency. For most new React apps that aren't already on Redux, React Query is the dominant choice.
- You're in a non-React app or non-Redux ecosystem.
- You only have 2–3 endpoints — manual fetch is fine.
Simple rule
- Transport layer? axios or fetch.
- Data-fetching layer (cache, dedup, state)? RTK Query if you already use Redux, React Query/SWR otherwise.
- Just doing one fetch on mount and never again? Plain fetch.
The mistake is treating axios as the "data-fetching layer." It's not. It transports bytes. Caching, dedup, invalidation, loading state — that's what RTKQ / React Query / SWR handle.
Follow-up questions
- •How does RTKQ's tag-based invalidation actually work?
- •What's the difference between RTKQ and React Query?
- •How do you do optimistic updates with RTKQ?
- •When would you reach for plain fetch over RTKQ?
Common mistakes
- •Using RTKQ for one or two endpoints — overkill for the bundle and learning curve.
- •Adding axios on top of RTKQ — RTKQ already has fetchBaseQuery; pick one transport.
- •Forgetting to set providesTags / invalidatesTags — mutations don't refresh related queries.
- •Disabling cache (refetchOnMount: 'always' everywhere) — loses RTKQ's main value.
- •Using RTKQ in a non-Redux app and pulling in all of Redux for it — use React Query instead.
- •Not handling skip/conditional fetch — queries fire when args are undefined.
Performance considerations
- •RTKQ's cache eliminates duplicate requests across components, which is the single biggest perf win vs hand-rolled fetch. Tag invalidation lets you refetch only what's affected by a mutation, instead of dumping the whole cache. Normalization avoids deep re-renders. Bundle cost is small (~9KB) when amortized across an app of any size; not justified for tiny apps.
Edge cases
- •selectFromResult for memoized derived state.
- •Lazy queries (useLazyGetUserQuery) for on-demand fetches.
- •Polling pauses when the tab is hidden (configurable).
- •Streaming updates (e.g. server-pushed WS) integrate via updateCachedData inside onCacheEntryAdded.
- •Code-gen from OpenAPI keeps endpoint types in sync with backend.
Real-world examples
- •Redux Toolkit official docs and example apps use RTKQ as the default data layer.
- •Many large enterprise React+Redux apps moved from a custom thunk-based data layer to RTKQ to delete thousands of lines of boilerplate.
- •React Query is the equivalent for non-Redux apps — same mental model, same hook ergonomics.