| | |

Redux Toolkit 13 ⚛️ RTK Query Setup — Configuring the API slice and base query

⏱️ Estimated read: 12 min | |

RTK Query is the data-fetching and caching layer built into Redux Toolkit. It is not a separate library; it is an additional addon that ships with @reduxjs/toolkit, and it leverages the same slice and middleware architecture that the previous chapters covered . The setup is a single createApi call that defines the base URL, the endpoints, and the tag types, and it generates a reducer, a middleware, and a set of React hooks. The previous chapters covered createSlice and createAsyncThunk, which require the developer to write the thunk, the lifecycle handlers, and the cache invalidation manually. RTK Query generates all of that from the endpoint definitions.

The createApi function is the core. It accepts a reducerPath, a baseQuery, and an endpoints builder, and it returns an object with the generated reducer, middleware, and hooks . The fetchBaseQuery helper is the standard base query, and it wraps the native fetch API with a base URL, default headers, and response parsing. The API slice is added to the store like any other reducer, and the middleware is concatenated to the default middleware.

Key point: The createApi call must be made before the store is created, because the store’s reducer object references the generated reducer. The reducerPath is the key under which the reducer is mounted, and it must match the path that the generated selectors and middleware expect. The baseQuery is the function that performs the actual request, and the fetchBaseQuery helper is the standard implementation. The tagTypes array defines the tags that the endpoints can provide and invalidate, and it is the mechanism that connects the queries and the mutations for automatic refetching.


Why RTK Query requires a different setup from createSlice

The generated-reducer problem.

A createSlice call returns a slice object with a reducer, actions, and a name. The developer adds the reducer to the store and dispatches the actions. A createApi call returns an API object with a reducer, a middleware, and hooks, and the developer adds the reducer to the store and concatenates the middleware . The middleware is not optional: it is the component that intercepts the dispatched actions, performs the fetch, and updates the cache. Without it, the queries never run, and the hooks return the initial state forever.

The single-slice-per-base-URL problem.

The RTK Query documentation recommends one API slice per base URL, not one per resource. A single createApi with baseUrl: '/api/' defines the posts, the users, and the comments endpoints, and the tag relationships connect them . The alternative—a separate createApi for each resource—creates separate caches and separate middleware, and the invalidation does not work across the slices. The injectEndpoints method is the mechanism for splitting the endpoint definitions across files while keeping a single API slice .

The TypeScript-inference problem.

The createApi call cannot reference the RootState type, because the store is created after the API slice. The getState inside the base query is typed as unknown, and the developer must cast it to RootState if the base query needs to read the state . The generated hooks are typed from the endpoint definitions, and the useGetPostsQuery hook returns the data type that the endpoint’s query generic specifies.


a. The createApi call and the base query

The createApi call defines the API slice. The reducerPath is the store key, the baseQuery is the request function, and the endpoints builder defines the queries and mutations.

// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export interface Post {
  id: number
  title: string
  body: string
}

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
    }),
    getPost: build.query<Post, number>({
      query: (id) => `posts/${id}`,
    }),
    addPost: build.mutation<Post, Pick<Post, 'title'>>({
      query: (body) => ({
        url: 'posts',
        method: 'POST',
        body,
      }),
    }),
  }),
})

export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = api

The reducerPath: 'api' is the key that the store uses to mount the reducer. The baseQuery: fetchBaseQuery({ baseUrl: '/api/' }) is the standard fetch wrapper, and the baseUrl is prepended to every endpoint’s URL . The tagTypes: ['Post'] declares the tag that the endpoints can provide and invalidate. The build.query and build.mutation methods define the endpoints, and the generated hooks are exported from the API object .

The fetchBaseQuery helper accepts a prepareHeaders function that runs before every request. It is the standard place to add the authorization header.

const baseQuery = fetchBaseQuery({
  baseUrl: '/api/',
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token
    if (token) headers.set('authorization', `Bearer ${token}`)
    return headers
  },
})

The prepareHeaders function receives the Headers object and the API object, and it can read the state with getState . The cast to RootState is necessary because the createApi call cannot reference the store’s type . The function returns the headers, and the base query uses them for the request.


b. Adding the API slice to the store

The API slice generates a reducer and a middleware, and both must be added to the store. The reducer is mounted under the reducerPath, and the middleware is concatenated to the default middleware.

// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from '../services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
})

setupListeners(store.dispatch)

The [api.reducerPath]: api.reducer line mounts the generated reducer under the api key. The getDefaultMiddleware().concat(api.middleware) line adds the RTK Query middleware, which is the component that handles the caching, the invalidation, and the polling . The setupListeners function enables the refetchOnFocus and refetchOnReconnect behaviors, and it must be called with the store’s dispatch .

The setupListeners call is optional but recommended. Without it, the queries do not refetch when the window regains focus or the network reconnects. The function is imported from @reduxjs/toolkit/query, not from the React entry point .


c. The tag types and the invalidation setup

The tagTypes array declares the tags that the endpoints can provide and invalidate. The tags are the mechanism that connects the queries and the mutations, and they are the reason the cache updates automatically after a write.

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
      providesTags: (result) =>
        result
          ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }]
          : [{ type: 'Post', id: 'LIST' }],
    }),
    addPost: build.mutation<Post, Pick<Post, 'title'>>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: [{ type: 'Post', id: 'LIST' }],
    }),
  }),
})

The providesTags function on the getPosts query returns a list of tags, one for each post and one for the list as a whole. The invalidatesTags on the addPost mutation returns the list tag, which tells RTK Query to refetch the queries that provide that tag . The 'Post' as const cast is necessary in TypeScript because the tagTypes array is typed as a union of string literals, and the spread array would otherwise broaden the type to string .

The invalidation is automatic. When the mutation succeeds, RTK Query refetches the getPosts query, and the component that uses the query re-renders with the new data. The developer does not need to call invalidateQueries or refetch manually.


Complete Example Session

// ============================================
// PART 1: THE API DEFINITION
// ============================================
// src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export interface Post {
  id: number
  title: string
  body: string
}

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
      providesTags: (result) =>
        result
          ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }]
          : [{ type: 'Post', id: 'LIST' }],
    }),
    addPost: build.mutation<Post, Pick<Post, 'title'>>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: [{ type: 'Post', id: 'LIST' }],
    }),
  }),
})

export const { useGetPostsQuery, useAddPostMutation } = api

// ============================================
// PART 2: THE STORE
// ============================================
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from '../services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
})

setupListeners(store.dispatch)

// ============================================
// PART 3: THE COMPONENT
// ============================================
// src/components/Posts.tsx
import { useGetPostsQuery, useAddPostMutation } from '../services/api'

function Posts() {
  const { data: posts = [], isLoading, isError } = useGetPostsQuery()
  const [addPost] = useAddPostMutation()

  if (isLoading) return <p>Loading...</p>
  if (isError) return <p>Error loading posts</p>

  return (
    <div>
      <button onClick={() => addPost({ title: 'New post' })}>Add</button>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

The three parts demonstrate the complete RTK Query setup: the API definition with the base query and the endpoints, the store with the reducer and the middleware, and the component with the generated hooks. The critical configuration is the tagTypes and the providesTags/invalidatesTags pair, which is the mechanism that connects the query and the mutation for automatic refetching.


Quick Reference

createApi Options

OptionPurposeExample
reducerPathStore key for the reducer'api'
baseQueryRequest functionfetchBaseQuery({ baseUrl: '/api/' })
tagTypesTags for invalidation['Post']
endpointsQuery and mutation definitions(build) => ({ ... })

Store Setup

StepCode
Add reducer[api.reducerPath]: api.reducer
Add middlewaregetDefaultMiddleware().concat(api.middleware)
Enable listenerssetupListeners(store.dispatch)

Generated Hooks

HookTypeReturns
useGetPostsQueryQuery{ data, isLoading, isError }
useAddPostMutationMutation[addPost, { isLoading }]

Tag Configuration

EndpointTag OptionExample
QueryprovidesTags[{ type: 'Post', id: 'LIST' }]
MutationinvalidatesTags[{ type: 'Post', id: 'LIST' }]

Best Practices

✅ Do This:

// Use one API slice per base URL
createApi({ baseUrl: '/api/' })                                # ✅

// Add the reducer and the middleware to the store
[api.reducerPath]: api.reducer
getDefaultMiddleware().concat(api.middleware)                  # ✅

// Call setupListeners for focus and reconnect refetch
setupListeners(store.dispatch)                                 # ✅

// Use tagTypes for invalidation
tagTypes: ['Post']                                             # ✅

// Use providesTags on queries and invalidatesTags on mutations
providesTags: [{ type: 'Post', id: 'LIST' }]
invalidatesTags: [{ type: 'Post', id: 'LIST' }]                # ✅

❌ Don’t Do This:

// Forget the middleware
// The queries never run                                     # ❌

// Create multiple API slices for the same base URL
const postsApi = createApi({ ... })
const usersApi = createApi({ ... })    // Separate caches    # ❌

// Omit setupListeners
// Focus and reconnect refetch do not work                   # ❌

// Forget the tagTypes
// Invalidation does not work                                # ❌

// Use the API slice before the store is created
// The reducer references the store's state                  # ❌

Common Pitfalls

PitfallWhy It HappensFix
Queries never runMiddleware not addedconcat(api.middleware)
Invalidation does not worktagTypes missingAdd the tag types
Hooks return undefinedReducer not mounted[api.reducerPath]: api.reducer
Focus refetch does not worksetupListeners not calledCall it with store.dispatch
TypeScript error on tagsType broadened to stringUse 'Post' as const

Real-World Examples

1. Create the API slice

createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), tagTypes: ['Post'], endpoints: (build) => ({ ... }) })

2. Query endpoint

getPosts: build.query<Post[], void>({ query: () => 'posts' })

3. Mutation endpoint

addPost: build.mutation<Post, Pick<Post, 'title'>>({ query: (body) => ({ url: 'posts', method: 'POST', body }) })

4. Provide tags

providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }] : [{ type: 'Post', id: 'LIST' }]

5. Invalidate tags

invalidatesTags: [{ type: 'Post', id: 'LIST' }]

6. Add to the store

reducer: { [api.reducerPath]: api.reducer }

7. Add the middleware

middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware)

8. Setup listeners

setupListeners(store.dispatch)

9. Use the query hook

const { data, isLoading } = useGetPostsQuery()

10. Use the mutation hook

const [addPost] = useAddPostMutation()

Visual

The API Slice and the Store

┌──────────────────────────────────────────────────────────────┐
│  createApi GENERATES THE REDUCER AND THE MIDDLEWARE          │
│                                                              │
│  createApi({ reducerPath: 'api', ... })                      │
│       │                                                      │
│       ├── api.reducer    → mounted at state.api              │
│       │                                                      │
│       └── api.middleware → concatenated to the store         │
│                                                              │
│  configureStore({                                            │
│      reducer: { [api.reducerPath]: api.reducer },            │
│      middleware: (gDM) => gDM().concat(api.middleware),      │
│  })                                                          │
│                                                              │
│  Without the middleware, the queries never run.              │
└──────────────────────────────────────────────────────────────┘

The Tag Invalidation Flow

┌──────────────────────────────────────────────────────────────┐
│  providesTags AND invalidatesTags CONNECT THE ENDPOINTS      │
│                                                              │
│  getPosts query:                                             │
│  └── providesTags: [{ type: 'Post', id: 'LIST' }]            │
│                                                              │
│  addPost mutation:                                           │
│  └── invalidatesTags: [{ type: 'Post', id: 'LIST' }]         │
│                                                              │
│  When addPost succeeds:                                      │
│  → RTK Query finds the queries that provide the tag          │
│  → The getPosts query is refetched                           │
│  → The component re-renders with the new data                │
└──────────────────────────────────────────────────────────────┘

The Generated Hooks

┌──────────────────────────────────────────────────────────────┐
│  EACH ENDPOINT GENERATES A HOOK                              │
│                                                              │
│  build.query<Post[], void>({ ... })                          │
│  └── useGetPostsQuery()                                     │
│      └── returns { data, isLoading, isError, refetch }       │
│                                                              │
│  build.mutation<Post, Pick<Post, 'title'>>({ ... })          │
│  └── useAddPostMutation()                                   │
│      └── returns [addPost, { isLoading, isError, reset }]    │
│                                                              │
│  The hooks are exported from the API object.                 │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Core functioncreateApi
Base query helperfetchBaseQuery
Store keyreducerPath
Tag declarationtagTypes
Query definitionbuild.query<Data, Arg>
Mutation definitionbuild.mutation<Data, Arg>
Query tagprovidesTags
Mutation taginvalidatesTags
Store reducer[api.reducerPath]: api.reducer
Store middlewaregetDefaultMiddleware().concat(api.middleware)
Listener setupsetupListeners(store.dispatch)

Key takeaways:

  • RTK Query is included in Redux Toolkit, not a separate library. The createApi function is the core, and it generates a reducer, a middleware, and a set of React hooks from the endpoint definitions .
  • The API slice must be added to the store with both the reducer and the middleware. The reducer is mounted under the reducerPath, and the middleware is concatenated to the default middleware. Without the middleware, the queries never run .
  • The fetchBaseQuery helper is the standard base query. It wraps the native fetch API with a base URL, default headers, and response parsing. The prepareHeaders function is the place to add the authorization header .
  • The tagTypes and the providesTags/invalidatesTags pair connect the queries and the mutations. The query provides the tags, and the mutation invalidates them. When the mutation succeeds, RTK Query refetches the queries that provide the invalidated tags .
  • The setupListeners call enables the focus and reconnect refetch behaviors. It is imported from @reduxjs/toolkit/query, and it must be called with the store’s dispatch. Without it, the queries do not refetch when the window regains focus or the network reconnects .
  • One API slice per base URL is the recommended pattern. The injectEndpoints method is the mechanism for splitting the endpoint definitions across files while keeping a single cache and a single middleware .

Remember: RTK Query setup is a single createApi call and two lines in the store. The createApi call defines the base query, the endpoints, and the tag types, and it generates the reducer, the middleware, and the hooks. The store mounts the reducer under the reducerPath and concatenates the middleware. The setupListeners call enables the focus and reconnect refetch. The tag types connect the queries and the mutations for automatic refetching. The result is a data-fetching layer that handles the caching, the loading states, and the invalidation without the thunks and the lifecycle handlers that the previous chapters required.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!