Concept
Every prior redux-toolkit topic in this domain, createAsyncThunk, Entity Adapter, was building toward the actual, common shape of a real app's data-fetching needs: loading states, caching, avoiding duplicate requests, and invalidating stale data after a mutation. Hand-assembling all of that from thunks and entity adapters works, but requires re-solving the same problems (dedup, cache invalidation, loading/error tracking) on every feature. RTK Query (createApi) is Redux Toolkit's purpose-built data-fetching and caching layer that solves all of it as one cohesive system.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["User"],
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `users/${id}`,
providesTags: (result, error, id) => [{ type: "User", id }],
}),
renameUser: builder.mutation({
query: ({ id, name }) => ({ url: `users/${id}`, method: "PATCH", body: { name } }),
invalidatesTags: (result, error, { id }) => [{ type: "User", id }],
}),
}),
});
export const { useGetUserQuery, useRenameUserMutation } = api;Confirmed by inspecting the generated api object directly: createApi (from the React-specific entry point) auto-generates hooks named by convention, useGetUserQuery for the getUser query endpoint, useRenameUserMutation for the renameUser mutation endpoint, following a fixed use${EndpointName}Query/use${EndpointName}Mutation naming pattern, with zero manual hook-writing required.
const fetchUser = createAsyncThunk('user/fetch', async (id) => {const res = await api.getUser(id);return res.data;});store.dispatch(fetchUser(1));
Dispatching a thunk created via createAsyncThunk looks like a single dispatch, but confirmed by execution, it actually fires a SEQUENCE of real, distinct actions.
Confirmed: identical query args are served from cache, the underlying function runs ONCE
store.dispatch(api.endpoints.getUser.initiate(1)); // fetches, queryFn runs
store.dispatch(api.endpoints.getUser.initiate(1)); // SAME arg, served from CACHE, queryFn does NOT run again
store.dispatch(api.endpoints.getUser.initiate(2)); // DIFFERENT arg, new cache entry, queryFn runsConfirmed by execution: dispatching getUser.initiate(1) twice in a row results in the underlying fetch logic actually executing once, the second call recognizes the identical serialized argument as a cache hit and returns the already-cached result directly. Dispatching with a different argument (2) creates a genuinely separate cache entry and does trigger a real fetch. RTK Query serializes each endpoint's arguments into a cache key (visible directly in the store's state, confirmed as "getUser(1)" and "getUser(2)"), and every component calling useGetUserQuery(1) anywhere in the app shares that exact same cache entry and in-flight request, no manual deduplication needed.
Confirmed: tag-based invalidation auto-refetches subscribed queries
providesTags: (result, error, id) => [{ type: "User", id }] // getUser(1) PROVIDES tag { type: "User", id: 1 }
invalidatesTags: (result, error, { id }) => [{ type: "User", id }] // renameUser INVALIDATES that same tagConfirmed by execution: with an active subscription to getUser(1) (a component still mounted and calling the hook, or an un-unsubscribed initiate() dispatch), dispatching renameUser.initiate({ id: 1, name: "Ada Lovelace" }), whose invalidatesTags matches getUser(1)'s providesTags on both type and id, automatically triggers a real refetch of getUser(1) shortly after the mutation settles, with no manually-written refetch call anywhere. The cached data for getUser(1) updates to reflect the new name, entirely from this tag match, this is the mechanism that replaces manually tracking "what needs to refetch after this mutation" by hand.
The React hooks: useGetUserQuery and useRenameUserMutation
function UserProfile({ userId }) {
const { data: user, isLoading, isError, error } = useGetUserQuery(userId);
const [renameUser, { isLoading: isRenaming }] = useRenameUserMutation();
if (isLoading) return <Spinner />;
if (isError) return <ErrorBanner error={error} />;
return (
<div>
<span>{user.name}</span>