In Redux Toolkit, what does createSlice generate when you convert a hand-written todos reducer into a slice?
answer
- one call, three hand-written parts gone
- name plus case reducer name
- actions object and reducer
- extraReducers takes a builder callback
basics
~20 screateSlice 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 sYou 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 linesimport { 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
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.
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.
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.
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