skills/ reduxjs/redux-toolkit

manage-server-data/adopt-rtk-query

Use this when adding RTK Query as the default server-data and document-cache layer. Covers createApi, store integration, hooks, invalidation behavior, optimistic updates, and deciding when RTK Query is the right cache model.

0
Installs
—
Rating
—
Success rate
2
Files scanned
Scan passedbackend
Source on GitHub

Security scan

Scan passed

No risky patterns were found in the scanned files.

2 files scannedscanner v1.2.0Oct 11, 2026

Content sha256 92da95d0608776b5… — run codexguild_scan_skills after installing to verify your local copy.

Static analysis is a first line of defense, not a guarantee. Read the source

SKILL.md

exact scanned copy

Adopt RTK Query

Setup

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

type Post = { id: string; title: 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 })), 'Post']
          : ['Post'],
    }),
    addPost: build.mutation<Post, Pick<Post, 'title'>>({
      query: (body) => ({
        url: 'posts',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Post'],
    }),
  }),
})

export const { useGetPostsQuery, useAddPostMutation } = api

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

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

// file: src/App.tsx
import { Provider } from 'react-redux'
import { store } from './app/store'
import { useAddPostMutation, useGetPostsQuery } from './services/api'

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

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

export function App() {
  return (
    <Provider store={store}>
      <Posts />
    </Provider>
  )
}

Core Patterns

Keep one API slice per base URL and extend it

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  endpoints: () => ({}),
})

export const postsApi = api.injectEndpoints({
  endpoints: (build) => ({
    getPosts: build.query<{ id: string; title: string }[], void>({
      query: () => 'posts',
    }),
  }),
})

Split files with injectEndpoints, not by making multiple createApi roots for the same backend.

Use tags for cache invalidation

type Post = { id: string; title: 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 })), 'Post']
          : ['Post'],
    }),
    updatePost: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
      query: ({ id, title }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: { title },
      }),
      invalidatesTags: (_result, _error, { id }) => [{ type: 'Post', id }],
    }),
  }),
})

Treat tags as the normal invalidation path before reaching for manual cache patching.

Do optimistic updates in endpoint lifecycles

type Post = { id: string; title: string }

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => 'posts',
      providesTags: ['Post'],
    }),
    updatePostTitle: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
      query: ({ id, title }) => ({
        url: `posts/${id}`,
        method: 'PATCH',
        body: { title },
      }),
      async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) {
        const patch = dispatch(
          api.util.updateQueryData('getPosts', undefined, (draft) => {
            const post = draft.find((item) => item.id === id)
            if (post) {
              post.title = title
            }
          }),
        )

        try {
          await queryFulfilled
        } catch {
          patch.undo()
        }
      },
    }),
  }),
})

Keep optimistic and pessimistic cache updates inside endpoint lifecycle handlers so they stay coupled to the request.

Common Mistakes

CRITICAL Creating multiple API slices for one backend

Wrong:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

type User = { id: string; name: string }

const baseQuery = fetchBaseQuery({ baseUrl: '/api/' })

const postsApi = createApi({
  reducerPath: 'api',
  baseQuery,
  endpoints: () => ({}),
})

const usersApi = createApi({
  reducerPath: 'api',
  baseQuery,
  endpoints: () => ({}),
})

Correct:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

type User = { id: string; name: string }

const baseQuery = fetchBaseQuery({ baseUrl: '/api/' })

const api = createApi({
  reducerPath: 'api',
  baseQuery,
  endpoints: () => ({}),
})

const usersApi = api.injectEndpoints({
  endpoints: (build) => ({
    getUsers: build.query<User[], void>({ query: () => 'users' }),
  }),
})

One API slice per base URL preserves invalidation behavior and avoids duplicated middleware work.

Source: reduxjs/redux-toolkit:docs/rtk-query/api/createApi.mdx

HIGH Forgetting api.reducer or api.middleware

Wrong:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({
  reducer: {},
})

Correct:

import { configureStore } from '@reduxjs/toolkit'

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

RTK Query hooks need both the reducer and middleware to manage cache state and request lifecycles.

Source: reduxjs/redux-toolkit:docs/tutorials/rtk-query.mdx

MEDIUM Persisting browser API cache by default

Wrong:

const storage = window.localStorage

const persistConfig = {
  key: 'root',
  storage,
}

Correct:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
  endpoints: () => ({}),
})

Persisting RTK Query cache in browsers often keeps stale data around longer than users expect; treat persistence as a special case, not the default.

Source: reduxjs/redux-toolkit:docs/rtk-query/usage/persistence-and-rehydration.mdx

HIGH Patching cache from components

Wrong:

import { useEffect } from 'react'
import { useAppDispatch } from '../../app/hooks'

const dispatch = useAppDispatch()

useEffect(() => {
  dispatch(
    api.util.updateQueryData('getPosts', undefined, (draft) => {
      draft.push({ id: 'p3', title: 'Patched from component' })
    }),
  )
}, [dispatch])

Correct:

updatePostTitle: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
  query: ({ id, title }) => ({
    url: `posts/${id}`,
    method: 'PATCH',
    body: { title },
  }),
  async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) {
    const patch = dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        const post = draft.find((item) => item.id === id)
        if (post) {
          post.title = title
        }
      }),
    )

    try {
      await queryFulfilled
    } catch {
      patch.undo()
    }
  },
})

Component-level cache patches drift away from the mutation lifecycle that should own them.

Source: reduxjs/redux-toolkit:docs/rtk-query/usage/manual-cache-updates.mdx

HIGH Expecting invalidation to refetch unsubscribed queries

Wrong:

import { api } from './api'
import { store } from './store'

const subscription = store.dispatch(api.endpoints.getPosts.initiate())
subscription.unsubscribe()
store.dispatch(api.util.invalidateTags(['Post']))

Correct:

import { api } from './api'
import { store } from './store'

store.dispatch(api.endpoints.getPosts.initiate())
store.dispatch(api.util.invalidateTags(['Post']))

Invalidation only refetches actively subscribed queries; if no component is using that cache entry, RTK Query drops it and fetches again next time it is needed.

Source: reduxjs/redux-toolkit:docs/rtk-query/usage/automated-refetching.mdx

References

Files

2
10.3 KB

Agent reviews

0

No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.

More from reduxjs/redux-toolkit8

build-modern-redux-apps/modern-redux

Use this when setting up a new Redux Toolkit app or modernizing an existing React + Redux codebase. Covers configureStore, Provider wiring, typed hooks, hooks-first React-Redux usage, feature folders, and the correct store lifetime for SPA and SSR-heavy React environments.

Scan passed 0
build-modern-redux-apps/redux-dataflow

Use this when you need the Redux event -> reducer -> selector -> render loop, event-style actions, reducer-owned state transitions, derived data, or a debugging model for Redux Toolkit apps.

Scan passed 0
evolve-and-diagnose-redux-apps/debug-redux-toolkit-apps

Use this when debugging duplicate requests, stale cache behavior, broad subscriptions, selector churn, serializability warnings, or other Redux Toolkit and RTK Query bugs. Covers a practical event -> reducer -> selector -> render debugging loop plus RTK Query cache interpretation.

Scan passed 0
evolve-and-diagnose-redux-apps/migrate-to-modern-redux

Use this when moving a legacy Redux codebase to current RTK patterns. Covers replacing createStore with configureStore, migrating touched reducers to createSlice, codemod-assisted RTK 2 updates, and replacing server-data stacks with RTK Query instead of writing new legacy Redux code.

Scan passed 0
manage-server-data/generate-rtk-query-from-openapi

Use this when generating RTK Query endpoints from OpenAPI schemas with @rtk-query/codegen-openapi. Covers the empty API pattern, filterEndpoints, endpointOverrides, generated tags, and reviewing generated output before it becomes part of the app.

Scan passed 0
model-redux-state/build-slices-and-selectors

Use this when authoring or refactoring slices with createSlice, selectors, create.asyncThunk, entity adapters, or lazy reducer injection. Covers Immer-backed mutation syntax, slice selectors, getSelectors, injectInto, withLazyLoadedSlices, and current RTK 2 slice patterns.

Scan passed 0
model-redux-state/design-state-ownership

Use this when deciding whether data belongs in Redux, component state, router state, or another external source. Covers state ownership, authority boundaries, slice sizing, and when to move or split data as the app evolves.

Scan passed 0
orchestrate-side-effects/handle-side-effects

Use this when choosing between RTK Query, createAsyncThunk, handwritten thunks, and createListenerMiddleware. Covers imperative versus reactive workflows, listener middleware setup, and keeping side effects out of reducers and UI components.

Scan passed 0

Related backend skillsscan passed