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.

  • Frontend
  • $redux-toolkit
  • 209 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- redux-toolkit

Installs to ~/.agents/skills/redux-toolkit/ for every project on this machine.

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.

~/.agents/skills/redux-toolkit/SKILL.md

--- name: redux-toolkit description: Writes and modernizes Redux with Redux Toolkit 2 — configureStore, createSlice with builder extraReducers, typed hooks via withTypes, memoized… license: MIT ---

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

  1. Read AGENTS.md, then versions: npm ls @reduxjs/toolkit react-redux redux. RTK 2 requires the builder callback for extraReducers and createReducer (the object syntax was removed), ships Reselect 5 and Redux 5, and pairs with React-Redux 9.
  2. Locate the store setup and list reducers: grep -rn "combineReducers\|configureStore\|createStore" src.
  3. 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: createEntityAdapter gives ids/entities, CRUD reducers and getSelectors.

Selectors

  • Select the smallest value: useAppSelector((s) => s.ui.sidebarOpen).
  • Selectors that return new arrays or objects (filter, map) must be memoized with createSelector, 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 LIST tag pattern makes creates and deletes refresh lists while edits refresh only the affected item.
  • Optimistic update: in onQueryStarted, call dispatch(api.util.updateQueryData("getProject", id, (draft) => { draft.name = name })), then await queryFulfilled in a try/catch and call patch.undo() on failure.
  • Conditional fetching: pass skipToken as the argument instead of a skip flag when the argument may be undefined; it keeps types narrow.
  • Use isLoading for the first load and isFetching for background refetches.
  • For large APIs, split endpoints across files with api.injectEndpoints.
  • Generate endpoints from an OpenAPI spec with @rtk-query/codegen-openapi when 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
  • configureStore adds 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 extraReducers with builder.addCase("todos/ADD_TODO", ...) or addMatcher.
  • connect still 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 via npx needs 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 fulfilled actions duplicates the cache; read from the query hook or api.endpoints.x.select(arg).
  • Forgetting api.middleware makes 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 any in RootState.
  • 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem