How does RTK implement immutability?
Short answer
Redux Toolkit implements immutability through the Immer library, which lets you write "mutating" code, but actually creates an immutable copy of the state.
Reminder: what immutability is
Immutability is a principle where you can't change the existing state directly; you must create a new one:
Wrong:
state.count++ // mutation (changing an existing object)Correct (in plain Redux):
return { ...state, count: state.count + 1 } // new objectHow Immer works inside RTK
Here's what Immer does:
- On every reducer call, RTK passes it a "draft" of the state.
- This draft is a proxy object that tracks all changes.
- You can "mutate" it in the usual way:
state.value++
state.todos.push(todo)
state.user.name = 'Tim'- When the function exits, Immer automatically creates an immutable copy of the state, applying all the recorded changes.
- Result: the old state remains untouched, and a new one is created reflecting the changes.
Example (the magic of Immer)
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
// Write as if mutating
state.value++
}
}
})What RTK does under the hood:
import { produce } from 'immer'
function incrementReducer(state, action) {
return produce(state, draft => {
draft.value++
})
}produce() from Immer creates a new immutable object where value is incremented and everything else keeps the same references.
How produce() works
import { produce } from 'immer'
const base = { count: 1, user: { name: 'Tim' } }
const next = produce(base, draft => {
draft.count++
draft.user.name = 'John'
})
console.log(next)
// { count: 2, user: { name: 'John' } }
console.log(base)
// { count: 1, user: { name: 'Tim' } } did not changeImmer uses Proxy and structural sharing to efficiently copy only the changed branches of the state tree (not the entire object).
Why this is convenient
| Problem without Immer | Solution in RTK |
|---|---|
| Need to write return { ...state, ... } every time | You can write state.value++ |
| Hard to update nested objects | You simply access state.user.profile.name |
| Risk of accidental mutation | Immer protects against it |
| Code is hard to read | Reducer logic becomes "natural" and clean |
Comparison example
Plain Redux:
function reducer(state, action) {
return {
...state,
user: {
...state.user,
name: 'Alex'
}
}
}Redux Toolkit (Immer inside):
function reducer(state, action) {
state.user.name = 'Alex'
}The code became three times shorter, while immutability was preserved.
What else makes Immer useful
- Copies only the changed parts of the state, so it works efficiently.
- Supports complex structures: arrays, objects, nesting.
- Easily catches mutation errors in dev mode.
- Works with TypeScript without issues.
Visually
[current state]
↓ (Immer creates a draft via Proxy)
[draft: temporary copy]
↓ (you "mutate" it)
Immer records the changes
↓
[new immutable state]Summary
| What | How it works |
|---|---|
| Immer | A library that creates immutable copies via Proxy |
| RTK | Uses Immer inside createSlice() |
| Developer | Writes "mutating" code but gets immutability for free |
| Result | Less code, fewer bugs, cleaner logic |
Final formulation:
Redux Toolkit ensures state immutability with the help of Immer, which creates a "draft" Proxy object, tracks changes, and returns a new, safely copied state without altering the original.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.