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.
- 0
- Installs
- —
- Rating
- —
- Success rate
- 1
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 69eb0e7fd607363c… — 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
Debug Redux Toolkit Apps
Setup
import { configureStore, createAsyncThunk, createSlice } from '@reduxjs/toolkit'
type Post = { id: string; title: string }
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('/api/posts')
return (await response.json()) as Post[]
},
{
condition(_arg, { getState }) {
const state = getState() as RootState
return state.posts.status === 'idle'
},
},
)
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [] as Post[],
status: 'idle' as 'idle' | 'pending' | 'succeeded' | 'failed',
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'pending'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
},
})
export const store = configureStore({
reducer: {
posts: postsSlice.reducer,
},
})
type RootState = ReturnType<typeof store.getState>
Core Patterns
Debug in order: action -> reducer -> selector -> render
const selectPosts = (state: RootState) => state.posts.items
const selectPostsStatus = (state: RootState) => state.posts.status
store.dispatch(fetchPosts())
console.log(selectPostsStatus(store.getState()))
console.log(selectPosts(store.getState()))
If a component looks wrong, first verify the action fired, then the reducer state, then the selector result, then the render boundary.
Narrow subscriptions at the usage site
import { useAppSelector } from '../../app/hooks'
export function PostsList() {
const posts = useAppSelector((state) => state.posts.items)
const status = useAppSelector((state) => state.posts.status)
return (
<div>
<div>{status}</div>
<div>{posts.length}</div>
</div>
)
}
React-Redux behaves best when components select only the values they render and do it as close to usage as possible.
Interpret RTK Query invalidation correctly
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
type Post = { id: string; title: string }
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => 'posts',
providesTags: ['Post'],
}),
updatePost: build.mutation<Post, Pick<Post, 'id' | 'title'>>({
query: ({ id, title }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: { title },
}),
invalidatesTags: ['Post'],
}),
}),
})
If invalidation did not visibly refetch, check whether anything was still subscribed to that cache entry.
Common Mistakes
HIGH Dispatching fetch thunks from effects without a thunk-level guard
Wrong:
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
function PostsPage() {
const dispatch = useAppDispatch()
const postStatus = useAppSelector((state) => state.posts.status)
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [dispatch, postStatus])
return null
}
Correct:
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('/api/posts')
return (await response.json()) as Post[]
},
{
condition(_arg, { getState }) {
const state = getState() as RootState
return state.posts.status === 'idle'
},
},
)
React StrictMode can run effects twice in development, so the guard belongs in the thunk as well as the component.
Source: reduxjs/redux:docs/tutorials/essentials/part-5-async-logic.md
HIGH Ignoring serializable-state warnings
Wrong:
const initialState = {
lastSeen: new Date(),
pendingIds: new Set<string>(),
}
Correct:
const initialState = {
lastSeenIso: new Date().toISOString(),
pendingIds: [] as string[],
}
Non-serializable values break DevTools, replay, persistence, and equality assumptions in subtle ways.
Source: reduxjs/redux:docs/style-guide/style-guide.md
HIGH Selecting broad state in parents and threading props
Wrong:
import { useAppSelector } from '../../app/hooks'
function PostsPage() {
const postsState = useAppSelector((state) => state.posts)
return <PostsList items={postsState.items} status={postsState.status} />
}
Correct:
import { useAppSelector } from '../../app/hooks'
function PostsList() {
const items = useAppSelector((state) => state.posts.items)
const status = useAppSelector((state) => state.posts.status)
return (
<div>
<div>{status}</div>
<div>{items.length}</div>
</div>
)
}
Selecting whole slices high in the tree widens the subscription surface and pushes rerenders through props.
Source: reduxjs/redux:docs/style-guide/style-guide.md
MEDIUM Returning unstable objects from query selection logic
Wrong:
import { api } from '../../services/api'
const result = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data = [] }) => ({
posts: [...data],
}),
})
Correct:
import { api } from '../../services/api'
const result = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data = [] }) => ({
posts: data,
}),
})
New object and array references defeat memoization and make components rerender even when the underlying cached data did not change.
Source: reduxjs/redux:docs/tutorials/essentials/part-8-rtk-query-advanced.md
Files
1- SKILL.md
afb5437bf56.3 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from reduxjs/redux-toolkit8
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.
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.
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.
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.
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.
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.
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.
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.