skills/ reduxjs/redux-toolkit

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.

0
Installs
—
Rating
—
Success rate
2
Files scanned
Scan passedsecurity
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 0f063acfb874297d… — 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

Build Slices And Selectors

Setup

// file: src/app/createAppSlice.ts
import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit'

export const createAppSlice = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
})

// file: src/features/posts/postsSlice.ts
import { createSelector } from '@reduxjs/toolkit'
import { createAppSlice } from '../../app/createAppSlice'

type PostsState = {
  items: { id: string; title: string; published: boolean }[]
  status: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState: PostsState = {
  items: [],
  status: 'idle',
}

export const postsSlice = createAppSlice({
  name: 'posts',
  initialState,
  reducers: (create) => ({
    postAdded: create.reducer<{ id: string; title: string }>(
      (state, action) => {
        state.items.push({ ...action.payload, published: false })
      },
    ),
    fetchPosts: create.asyncThunk(
      async () => {
        const response = await fetch('/api/posts')
        return (await response.json()) as {
          id: string
          title: string
          published: boolean
        }[]
      },
      {
        pending: (state) => {
          state.status = 'pending'
        },
        fulfilled: (state, action) => {
          state.status = 'succeeded'
          state.items = action.payload
        },
        rejected: (state) => {
          state.status = 'failed'
        },
      },
    ),
  }),
  selectors: {
    selectPosts: (state) => state.items,
    selectPublishedPosts: createSelector(
      [(state: PostsState) => state.items],
      (items) => items.filter((post) => post.published),
    ),
  },
})

export const { postAdded, fetchPosts } = postsSlice.actions
export const { selectPosts, selectPublishedPosts } = postsSlice.selectors

Core Patterns

Use mutating logic inside slice reducers

import { createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as { id: string; text: string; done: boolean }[],
  reducers: {
    todoAdded(state, action: { payload: { id: string; text: string } }) {
      state.push({ ...action.payload, done: false })
    },
    todoToggled(state, action: { payload: { id: string } }) {
      const todo = state.find((item) => item.id === action.payload.id)
      if (todo) {
        todo.done = !todo.done
      }
    },
  },
})

Immer is the default inside createSlice; write the reducer logic directly instead of copying arrays and objects by hand.

Define selectors in the slice when they belong to the slice

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value += 1
    },
  },
  selectors: {
    selectValue: (state) => state.value,
    selectIsPositive: (state) => state.value > 0,
  },
})

const { selectValue, selectIsPositive } = counterSlice.selectors

Slice selectors keep state-location knowledge next to the slice.

Use create.asyncThunk when the async lifecycle belongs to the slice

import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit'

const createAppSlice = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
})

const usersSlice = createAppSlice({
  name: 'users',
  initialState: {
    items: [] as { id: string; name: string }[],
    status: 'idle' as 'idle' | 'pending' | 'failed',
  },
  reducers: (create) => ({
    fetchUsers: create.asyncThunk(
      async () => {
        const response = await fetch('/api/users')
        return (await response.json()) as { id: string; name: string }[]
      },
      {
        pending: (state) => {
          state.status = 'pending'
        },
        fulfilled: (state, action) => {
          state.status = 'idle'
          state.items = action.payload
        },
        rejected: (state) => {
          state.status = 'failed'
        },
      },
    ),
  }),
})

Use this when the async lifecycle handlers naturally live with the slice; otherwise regular createAsyncThunk is still fine.

Use entity adapters and lazy injection for scalable slices

import {
  combineSlices,
  createEntityAdapter,
  createSlice,
} from '@reduxjs/toolkit'

type Book = { bookId: string; title: string }

const booksAdapter = createEntityAdapter<Book>({
  selectId: (book) => book.bookId,
})

const booksSlice = createSlice({
  name: 'books',
  initialState: booksAdapter.getInitialState(),
  reducers: {
    booksReceived: booksAdapter.setAll,
  },
})

export interface LazyLoadedSlices {}

export const rootReducer =
  combineSlices().withLazyLoadedSlices<LazyLoadedSlices>()

declare module './rootReducer' {
  export interface LazyLoadedSlices {}
}

const injectedBooksSlice = booksSlice.injectInto(rootReducer)

const selectors = booksAdapter.getSelectors(
  (state: ReturnType<typeof rootReducer.selector.original>) =>
    injectedBooksSlice.selectSlice(state),
)

Entity adapters standardize normalized collections, and injectInto lets a slice stay aware of its injected location.

Common Mistakes

CRITICAL Using mutating logic outside slice reducers

Wrong:

type Todo = { id: string; text: string }

export function addTodo(todos: Todo[], todo: Todo) {
  todos.push(todo)
  return todos
}

Correct:

type Todo = { id: string; text: string }

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Todo[],
  reducers: {
    todoAdded(state, action: { payload: Todo }) {
      state.push(action.payload)
    },
  },
})

Mutation syntax is only safe inside Immer-backed reducer contexts such as createSlice and createReducer.

Source: reduxjs/redux-toolkit:docs/usage/immer-reducers.md

HIGH Writing hand-written switch reducers as the default

Wrong:

export default function todosReducer(
  state = initialState,
  action: { type: string; payload?: Todo },
) {
  switch (action.type) {
    case 'todos/todoAdded':
      return state.concat(action.payload as Todo)
    default:
      return state
  }
}

Correct:

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    todoAdded(state, action: { payload: Todo }) {
      state.push(action.payload)
    },
  },
})

Hand-written reducers are an escape hatch for proven bottlenecks, not the normal thing an agent should generate in RTK code.

Source: reduxjs/redux-toolkit:docs/usage/migrating-to-modern-redux.mdx

HIGH Writing RTK 1.x object syntax for extraReducers

Wrong:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

const initialState = { items: [] as { id: string; title: string }[] }

const fetchPosts = createAsyncThunk('posts/fetch', async () => {
  const response = await fetch('/api/posts')
  return (await response.json()) as { id: string; title: string }[]
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: {
    [fetchPosts.fulfilled.type]: (state, action) => {
      state.items = action.payload
    },
  },
})

Correct:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

const initialState = { items: [] as { id: string; title: string }[] }

const fetchPosts = createAsyncThunk('posts/fetch', async () => {
  const response = await fetch('/api/posts')
  return (await response.json()) as { id: string; title: string }[]
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.items = action.payload
    })
  },
})

RTK 2 removed the object form; agents trained on RTK 1.x still generate it.

Source: reduxjs/redux-toolkit:docs/usage/migrating-rtk-2.md

HIGH Assuming entity.id exists for every collection

Wrong:

type Book = { bookId: string; title: string }

const booksAdapter = createEntityAdapter<Book>()

Correct:

type Book = { bookId: string; title: string }

const booksAdapter = createEntityAdapter<Book>({
  selectId: (book) => book.bookId,
})

Adapters default to entity.id; collections keyed by another field must provide selectId.

Source: reduxjs/redux-toolkit:docs/api/createEntityAdapter.mdx

References

Files

2
10.5 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/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.

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/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 security skillsscan passed

django-security

Django security best practices, authentication, authorization, CSRF protection, SQL injection prevention, XSS prevention, and secure deployment configurations. Use when reviewing Django authentication, authorization, input handling, or deployment settings.

Scan passed 0
cso

Security audit: supported static findings; qualified profiles add reproduction and repair candidates. (gstack)

Scan passed 0
claude-security

Claude Security: scan the codebase (the whole repository or a scoped part of it), scan changes (this branch's or a pull request's diff, or one commit), or suggest patches (findings turned into targeted patch files, each verified by a panel of agents, that you apply when you choose). Use when the use

Scan passed 0
client-setup

Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer on links (not client constructor). Infer types with inferRouterInputs and inferRouterOutputs. AbortController signal support. TRPCClientError typin

Scan passed 0
security-and-hardening

Hardens code against vulnerabilities. Use when auditing an input handler for vulnerabilities, when handling user input, authentication, data storage, or external integrations, or when checking a login flow is safe against the OWASP Top Ten. Use when building any feature that accepts untrusted data,

Scan passed 0
ponytail-audit

Quality audit of a whole repo: bugs, security holes, what breaks under real load, risky code without tests, slow paths, and what to delete, merge or split. Ranked, each finding explained in plain English. One-shot report, changes nothing. Use for "audit this codebase", "review the whole repo", "find

Scan passed 0