Redux Toolkit skill for Codex
Slices, typed hooks and RTK Query without legacy boilerplate. Codex builds stores with configureStore and a makeStore factory, writes slices with builder extraReducers and slice selectors, and memoizes derived selectors so components stop re-rendering on every dispatch. Server data moves to RTK Query with an item-plus-LIST tag scheme and undoable optimistic patches, and legacy createStore code is converted reducer by reducer while the app keeps working.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- redux-toolkit Installs to ~/.agents/skills/redux-toolkit/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install redux-toolkit The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project redux-toolkit Installs to .agents/skills/redux-toolkit/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/redux-toolkit.zipunzip -o redux-toolkit.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) redux-toolkit PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $redux-toolkit, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and modernizes Redux with Redux Toolkit 2 — configureStore, createSlice with builder extraReducers, typed hooks via withTypes, memoized selectors, entity adapters, listener middleware, and RTK Query APIs with tags, cache lifetimes and optimistic updates — and migrates legacy createStore, switch reducers, connect and redux-thunk code incrementally. Use when adding a slice or RTK Query endpoint, when data is stale after a mutation, when upgrading to RTK 2 and React-Redux 9, or when converting an old hand-written Redux codebase.
Use it when
- Adding slices or RTK Query endpoints to a Redux app
- Data stays stale after a mutation in RTK Query
- Upgrading to RTK 2 and React-Redux 9
- Converting legacy hand-written Redux, connect and thunk code
Use something else when
- A new app needs only a little shared client state: use zustand-state
- The project already standardizes on TanStack Query for server data: use tanstack-query
How to check the result
Type-check passes, Redux DevTools shows no mutation or serializability warnings, and each mutation refetches only the queries whose tags it invalidates.
The mistake it prevents
Selectors that filter or map without createSelector, returning a new array on every dispatch and re-rendering the component each time.
Done well means: the store is built with configureStore, every reducer is a slice,
components use typed hooks and memoized selectors, server data goes through RTK Query
with explicit tags instead of hand-written thunks and loading flags, and legacy code is
migrated reducer by reducer with the app working at every step.
Before you start
- Read
AGENTS.md, then versions:npm ls @reduxjs/toolkit react-redux redux. RTK 2 requires the builder callback forextraReducersandcreateReducer(the object syntax was removed), ships Reselect 5 and Redux 5, and pairs with React-Redux 9. - Locate the store setup and list reducers:
grep -rn "combineReducers\|configureStore\|createStore" src. - Check how server data is fetched today (thunks, sagas, RTK Query, TanStack Query). Do not introduce a second server-state library without agreement.
Store and typed hooks
// src/app/store.ts
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { api } from "./api";
import { uiSlice } from "@/features/ui/ui-slice";
export const makeStore = () =>
configureStore({
reducer: { [uiSlice.reducerPath]: uiSlice.reducer, [api.reducerPath]: api.reducer },
middleware: (getDefault) => getDefault().concat(api.middleware),
});
export const store = makeStore();
setupListeners(store.dispatch); // refetchOnFocus / refetchOnReconnect support
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];
// src/app/hooks.ts
import { useDispatch, useSelector, useStore } from "react-redux";
import type { AppDispatch, AppStore, RootState } from "./store";
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppStore = useStore.withTypes<AppStore>();
For Next.js App Router, create the store per request with makeStore() inside a client
provider (useRef/useState initializer); never export a module-level store that the
server reuses across users.
Slices
import { createSlice, createAsyncThunk, type PayloadAction } from "@reduxjs/toolkit";
type UiState = { sidebarOpen: boolean; toasts: Toast[] };
const initialState: UiState = { sidebarOpen: true, toasts: [] };
export const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
toggleSidebar: (state) => { state.sidebarOpen = !state.sidebarOpen; }, // Immer: mutate the draft
toastAdded: {
reducer: (state, action: PayloadAction<Toast>) => { state.toasts.push(action.payload); },
prepare: (message: string) => ({ payload: { id: nanoid(), message } }),
},
},
extraReducers: (builder) => {
builder.addCase(logout.fulfilled, () => initialState);
},
selectors: {
selectSidebarOpen: (state) => state.sidebarOpen,
},
});
export const { toggleSidebar, toastAdded } = uiSlice.actions;
export const { selectSidebarOpen } = uiSlice.selectors;
- Inside reducers, either mutate the draft or return a new value, never both.
- Name actions as past-tense events (
toastAdded,orderSubmitted) when several slices react to them; setter names (setX) are fine for slice-local UI state. - Non-serializable values (Dates, class instances, Promises, functions) in state or actions trigger the serializability middleware warning; store ISO strings and IDs.
- Normalized collections:
createEntityAdaptergivesids/entities, CRUD reducers andgetSelectors.
Selectors
- Select the smallest value:
useAppSelector((s) => s.ui.sidebarOpen). - Selectors that return new arrays or objects (
filter,map) must be memoized withcreateSelector, or the component re-renders on every dispatch. React-Redux 9 warns in development when a selector returns a different reference for the same input.
export const selectVisibleTodos = createSelector(
[(s: RootState) => s.todos.items, (s: RootState) => s.todos.filter],
(items, filter) => items.filter((t) => (filter === "done" ? t.done : !t.done)),
);
RTK Query for server data
// src/app/api.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api", credentials: "include" }),
tagTypes: ["Project"],
keepUnusedDataFor: 60, // seconds an unused cache entry survives (default 60)
endpoints: (build) => ({
getProjects: build.query<Project[], { status: string }>({
query: ({ status }) => ({ url: "projects", params: { status } }),
providesTags: (result) =>
result
? [...result.map((p) => ({ type: "Project" as const, id: p.id })), { type: "Project", id: "LIST" }]
: [{ type: "Project", id: "LIST" }],
}),
getProject: build.query<Project, string>({
query: (id) => `projects/${id}`,
providesTags: (_r, _e, id) => [{ type: "Project", id }],
}),
renameProject: build.mutation<Project, { id: string; name: string }>({
query: ({ id, name }) => ({ url: `projects/${id}`, method: "PATCH", body: { name } }),
invalidatesTags: (_r, _e, { id }) => [{ type: "Project", id }, { type: "Project", id: "LIST" }],
}),
}),
});
export const { useGetProjectsQuery, useGetProjectQuery, useRenameProjectMutation } = api;
- The
LISTtag pattern makes creates and deletes refresh lists while edits refresh only the affected item. - Optimistic update: in
onQueryStarted, calldispatch(api.util.updateQueryData("getProject", id, (draft) => { draft.name = name })), thenawait queryFulfilledin a try/catch and callpatch.undo()on failure. - Conditional fetching: pass
skipTokenas the argument instead of askipflag when the argument may be undefined; it keeps types narrow. - Use
isLoadingfor the first load andisFetchingfor background refetches. - For large APIs, split endpoints across files with
api.injectEndpoints. - Generate endpoints from an OpenAPI spec with
@rtk-query/codegen-openapiwhen the backend publishes one.
Side effects
Prefer createListenerMiddleware over sagas for new reactive logic (sync to storage,
analytics, debounced autosave). Use createAsyncThunk for non-cache async flows such as
multi-step checkout. Keep existing sagas or observables unless the task is to remove them.
Migrating legacy Redux
- [ ] 1. Replace createStore + applyMiddleware + compose with configureStore
- [ ] 2. Keep old reducers mounted as-is; confirm the app works
- [ ] 3. Convert one reducer at a time to createSlice, keeping action type strings if
other code dispatches them by string
- [ ] 4. Replace hand-written thunks that fetch data with RTK Query endpoints
- [ ] 5. Move connect() components to hooks when you touch them
- [ ] 6. Delete action type constants and action creator files once unused
configureStoreadds thunk and dev-only immutability and serializability checks. The checks will surface existing mutations in old reducers; fix them, do not disable the middleware.- To keep compatibility while converting, a slice can respond to old string actions in
extraReducerswithbuilder.addCase("todos/ADD_TODO", ...)oraddMatcher. connectstill works in React-Redux 9; migrate it opportunistically, not in one sweep.- Codemods for RTK 2 syntax changes are published as
@reduxjs/rtk-codemods; running them vianpxneeds network access.
Gotchas
- Two store instances (one in tests, one in the app module) lead to tests passing while
the app is wrong. Build both with
makeStore(). - Putting RTK Query data into a slice by listening to
fulfilledactions duplicates the cache; read from the query hook orapi.endpoints.x.select(arg). - Forgetting
api.middlewaremakes queries fetch once and never refetch, poll or expire. - Tag matching is asymmetric: invalidating bare
"Project"refreshes every query that provides any Project tag, while invalidating{ type: "Project", id }refreshes only queries that provided that id or the bare"Project"tag. Write the provide and invalidate matrix down when lists refresh too much or too little.
Verification
- Type-check passes; no
anyinRootState. - Redux DevTools: actions have meaningful names; no serializability or mutation warnings in the console.
- After each mutation, only the expected queries refetch (DevTools RTK Query tab or Network panel).
- Tests create a fresh store per test via
makeStore().
Output
For new code: slices with their actions and selectors, endpoints with provided and invalidated tags. For migrations: a checklist of reducers converted, thunks replaced, and remaining legacy pieces.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Redux Toolkit"
short_description: "Slices, typed hooks and RTK Query without legacy boilerplate"
default_prompt: "$redux-toolkit Replace our fetchProjects thunks and loading flags with RTK Query endpoints" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.