skip to content

In Redux Toolkit, what does createSlice generate when you convert a hand-written todos reducer into a slice?

level: juniorimportance: must knowfreq 76%

answer

  1. one call, three hand-written parts gone
  2. name plus case reducer name
  3. actions object and reducer
  4. extraReducers takes a builder callback

basics

~20 s

createSlice takes a name, an initialState and a reducers object and returns the slice reducer plus one action creator per case reducer, with each type generated as name/caseName, replacing hand-written constants, action creators and the switch.

solid answer

~40 s

You call `createSlice({ name: 'todos', initialState, reducers: { todoAdded(state, action) {...}, todoToggled(...) {...} } })`. It returns a slice object whose `reducer` handles those cases and whose `actions` holds one generated action creator per case reducer. Each action type is built as `` `${name}/${caseName}` ``, so `todoAdded` dispatches `'todos/todoAdded'`. The action type constants, the hand-written action creators and the `switch` all disappear, and because the case reducers run through Immer you can write `state.push(action.payload)` instead of spreading arrays. Actions the slice did not define itself, such as another slice's action or a thunk's lifecycle actions, go in `extraReducers`, which in RTK 2 only accepts a builder callback.

code

ts · 27 lines
ts
import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit'

interface Todo { id: string; text: string; completed: boolean }

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Todo[],
  reducers: {
    todoAdded: {
      reducer(state, action: PayloadAction<Todo>) {
        state.push(action.payload)
      },
      prepare(text: string) {
        return { payload: { id: nanoid(), text, completed: false } }
      },
    },
    todoToggled(state, action: PayloadAction<string>) {
      const todo = state.find((t) => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    },
  },
})

export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer

// todoToggled('42') -> { type: 'todos/todoToggled', payload: '42' }

go deeper

for a junior

Recall the three inputs, name, initialState and reducers, and the two outputs you export, slice.actions and slice.reducer. Know that a generated type looks like todos/todoAdded.

for a middle

Explain the difference between reducers, which define and handle an action, and extraReducers, which only handle actions defined elsewhere, and show the prepare callback for payload shaping.

for a senior

Show judgment about slice boundaries: which actions a slice owns, how cross-slice reactions go through the builder, and why keeping ids and timestamps in prepare keeps reducers replayable.

for a principal

Frame createSlice as generated code over the unchanged Redux contract, so a migration can convert one reducer at a time while hand-written reducers keep working in the same store.

## The hand-written starting point A classic Redux todos feature is spread over three pieces that must agree with each other: - **action type constants** such as `const TODO_ADDED = 'todos/todoAdded'`; - **action creators** such as `todoAdded(todo)` returning `{ type: TODO_ADDED, payload: todo }`; - a **reducer** with a `switch (action.type)` whose every case copies the state immutably, for example `return [...state, action.payload]` or a `map` that rebuilds one todo with `completed` flipped. Each piece is simple, but the three are connected only by string matching. Renaming a case means touching three places, and a typo in a constant produces a reducer that silently ignores the action. ## What createSlice takes `createSlice` from `@reduxjs/toolkit` replaces all three with one call. Its main options are: 1. `name` — a string used as the prefix of every generated action type. 2. `initialState` — the slice's starting value (it can also be a lazy initializer function). 3. `reducers` — an object whose keys are case names and whose values are **case reducers**: functions `(state, action) => newState | void` that handle exactly one action. 4. `extraReducers` — optional, for actions the slice did **not** define itself. For object or array state, each case reducer receives an Immer draft, so the reducer can be written as if it mutated: `state.push(action.payload)` or `todo.completed = !todo.completed`. Immer turns those writes into a new immutable state; it is still immutable Redux underneath. ## What it returns | Field on the slice | What it is | |---|---| | `reducer` | the combined case reducer for this slice, ready to pass to `configureStore` | | `actions` | one action creator per key in `reducers` | | `caseReducers` | the original case reducer functions, handy for tests or reuse | | `name` / `reducerPath` | the slice name and the key it expects in the root state | | `getInitialState()` | returns the initial state value | Every generated action creator returns a plain action object, `{ type, payload }`, where the type is built as `` `${name}/${caseName}` ``. With `name: 'todos'` and a case named `todoAdded`, the type is `'todos/todoAdded'`. Calling the action creator only **builds** the action; you still `dispatch` it. Each creator also carries a `type` property and a `match(action)` type guard, so other code can recognise the action without importing a string constant. If an action needs its payload prepared before it reaches the reducer — for example generating an id with RTK's `nanoid` — you write the case as an object `{ reducer, prepare }`, and `prepare` returns `{ payload }` (optionally with `meta` and `error`). ## Responding to actions the slice did not define `reducers` both **defines** an action and **handles** it. When the slice must react to an action defined elsewhere — another slice's `userLoggedOut`, or the `pending`/`fulfilled`/`rejected` actions of a `createAsyncThunk` — it goes in `extraReducers`, which generates no action creators. - In **RTK 2**, `extraReducers` must be a **builder callback**: `extraReducers: (builder) => { builder.addCase(userLoggedOut, () => initialState) }`. - The older object form keyed by action type was removed; passing an object throws in development. - `builder.addCase` calls must come before any `builder.addMatcher`, and `builder.addDefaultCase` comes last. ## Where the slice plugs in The slice's `reducer` is mounted under a key in the store, for example `configureStore({ reducer: { todos: todosSlice.reducer } })`. That key should match the slice's `reducerPath`, which defaults to `name`. An RTK 2 slice can also declare a `selectors` field whose functions receive the slice state; `slice.selectors` exposes them wrapped to read from `rootState[reducerPath]`. Components then dispatch `todoAdded('Buy milk')` and read the list through a selector, exactly as they did before the conversion: nothing downstream needs to know that the reducer and its action creators were generated. ## What the conversion actually removed - The type constants, because types are derived from `name` and the case names. - The action creators, because `slice.actions` generates them. - The `switch`, because `createSlice` builds a lookup from type to case reducer. - Most spread-and-copy code, because Immer lets case reducers express updates as writes. What it did **not** remove is the Redux model itself: there is still one store, actions are still plain serialisable objects, reducers are still pure functions of `(state, action)`, and components still dispatch and select. `createSlice` is a code generator on top of that model, which is why a slice's reducer can be mounted next to a hand-written reducer in the same store. A good interview answer names the three things that disappear, gives the `name/caseName` rule for the generated type, and says where foreign actions go.

  • How do you make a Redux Toolkit slice reset itself when another slice's logout action is dispatched?
    Handle it in `extraReducers` with the builder: `builder.addCase(userLoggedOut, () => initialState)`. Returning a value replaces the slice state. Putting the case in `reducers` would not work, because a key there defines a new action type, `todos/userLoggedOut`, instead of listening to the existing one.
  • A Redux Toolkit case reducer needs a generated id and a timestamp. Where should that code live?
    In a `prepare` callback, by writing the case as `{ reducer, prepare }`. `prepare` receives the arguments passed to the action creator and returns `{ payload }`, so randomness stays out of the reducer and the reducer remains pure and replayable.
  • How does other code check whether an action came from a Redux Toolkit slice without importing type strings?
    Every generated action creator carries a `type` property and a `match(action)` type guard, so `todoAdded.match(action)` both tests the type and narrows the payload type in TypeScript. Builder `addCase` accepts the action creator directly for the same reason.

saying these in an interview costs you the question

  • Calling a generated action creator dispatches the action to the store
  • You still write action type constants next to a slice
  • The generated type is just the case name, such as todoAdded
  • extraReducers can still be an object keyed by action type in RTK 2
  • Listening to another slice's action means adding its name under reducers