This episode introduces RTK Query: creating an API client with createApi and fetchBaseQuery, defining endpoints.query, using the useGetPostsQuery hook in components, and understanding automatic caching, request deduplication, and the complete query status.

Up to episode 7 we handled API data manually: writing createAsyncThunk, managing loading state, and calling dispatch in components. Episode 8 introduces a higher-level solution — RTK Query. RTK Query is data fetching and caching tooling fully integrated with Redux Toolkit, designed to eliminate network boilerplate.
Why does RTK Query matter? Because the primary need of real applications isn't just local state — it's server state: data that must be fetched, cached, updated, and kept in sync. RTK Query manages the request lifecycle automatically: results are stored in a cache, identical requests aren't sent twice, and components never have to think about fetching status manually.
The center of RTK Query is createApi. Inside it we define reducerPath, baseQuery to handle HTTP, and endpoints containing every data operation. fetchBaseQuery is the built-in fetch wrapper that already handles URLs, headers, and error serialization:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: () => ({}),
})reducerPath determines the state key name in the store. baseUrl: "/api" means requests will go to /api/posts when we call the endpoints below. The endpoints function is filled in through the builder provided by RTK Query.
For query hooks to work, the result of createApi must be registered in the store. Three things need to be set up: the reducer, the middleware, and postsApi.middleware:
import { configureStore } from "@reduxjs/toolkit"
import { setupListeners } from "@reduxjs/toolkit/query"
import { postsApi } from "../services/postsApi"
import counterReducer from "../features/counter/counterSlice"
export const store = configureStore({
reducer: {
counter: counterReducer,
[postsApi.reducerPath]: postsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(postsApi.middleware),
})
setupListeners(store.dispatch)The RTK Query middleware handles caching, invalidation, and automatic re-fetching. The setupListeners function enables the refetchOnFocus and refetchOnReconnect features that are useful in real applications.
endpoints.query marks a data-reading operation. The builder accepts builder.query, which returns data from the server:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
export interface Post {
id: number
title: string
body: string
}
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => "posts",
}),
getPostById: builder.query<Post, number>({
query: (id) => `posts/${id}`,
}),
}),
})getPosts will call GET /api/posts, while getPostById calls /api/posts/{id}. The generic builder.query<Post[], void> means the result is an array of Post and the query argument is empty. The argument is used as the cache identifier: different values are stored in different cache slots.
For every query endpoint, RTK Query creates a hook with a predictable name:
export const { useGetPostsQuery, useGetPostByIdQuery } = postsApiThe naming pattern is use + capitalized endpoint + Query. These are the hooks we use in components without writing any dispatch or useEffect at all.
A query hook returns many status values at once: data, isLoading, isError, isSuccess, error, and refetch:
import { useGetPostsQuery } from "../services/postsApi"
export function PostList() {
const { data: posts, isLoading, isError, error, refetch } = useGetPostsQuery()
if (isLoading) return <p>Loading data...</p>
if (isError) return <p>An error occurred: {JSON.stringify(error)}</p>
if (isSuccess && posts) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
return <button onClick={refetch}>Reload</button>
}Notice the flow: during the first load isLoading is true, then it becomes isSuccess with data ready to use. refetch forces a re-fetch without deleting the existing cache.
Queries with arguments are simply passed as hook parameters. RTK Query also supports real-time polling through the pollingInterval option:
export function PostDetail({ id }: { id: number }) {
const { data: post } = useGetPostByIdQuery(id, {
pollingInterval: 30000,
})
return <article>{post ? post.body : "Loading..."}</article>
}The code above fetches post details every 30 seconds. A changing id value automatically triggers a new request because it becomes a different cache key.
RTK Query stores results in the store using a structure keyed by argument. Two different components using useGetPostsQuery() share a single cache entry — the server receives only one request. This is the request deduplication promised at the start.
npm ls @reduxjs/toolkit react-reduxThe cache doesn't live forever. Data is considered fresh for the default timeout of 60 seconds after loading. RTK Query combines several re-fetch mechanisms:
refetchOnMountOrArgChange: refetches when a component mounts with stale data.refetchOnFocus: returns data when the tab regains focus.refetchOnReconnect: refreshes when the internet connection is restored.keepUnusedDataFor: how long the cache is kept after there are no subscribers.All of these can be set globally in createApi or per-endpoint, for example refetchOnFocus: true when the application needs always-fresh data.
Tip
Check the Network tab in Redux DevTools: you'll see the /api/posts request sent only once even though several components use the same data. That's proof the deduplication is working.
Episode 8 shows that RTK Query removes the manual work of data fetching: createApi and fetchBaseQuery handle requests, query hooks provide complete status, and the cache system manages the data lifecycle automatically. Components no longer think about when to call dispatch — they just use the hook and render according to status.
Key takeaways:
createApi with fetchBaseQuery is the foundation of all RTK Query.setupListeners.query for reading and mutation for writing.useGetXQuery hook returns data, isLoading, isError, isSuccess, and refetch.refetchOnFocus, refetchOnReconnect, and polling control when data is re-fetched.In the next episode, episode 9, we learn mutations & cache invalidation — how to send data to the server with useXMutation, apply optimistic updates through onQueryStarted, and keep data lists in sync with providesTags and invalidatesTags after write operations.