Skip to main content

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:

javascript
state.count++ // mutation (changing an existing object)

Correct (in plain Redux):

javascript
return { ...state, count: state.count + 1 } // new object

How Immer works inside RTK

Here's what Immer does:

  1. On every reducer call, RTK passes it a "draft" of the state.
  2. This draft is a proxy object that tracks all changes.
  3. You can "mutate" it in the usual way:
javascript
state.value++ state.todos.push(todo) state.user.name = 'Tim'
  1. When the function exits, Immer automatically creates an immutable copy of the state, applying all the recorded changes.
  2. Result: the old state remains untouched, and a new one is created reflecting the changes.

Example (the magic of Immer)

javascript
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:

javascript
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

javascript
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 change

Immer 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 ImmerSolution in RTK
Need to write return { ...state, ... } every timeYou can write state.value++
Hard to update nested objectsYou simply access state.user.profile.name
Risk of accidental mutationImmer protects against it
Code is hard to readReducer logic becomes "natural" and clean

Comparison example

Plain Redux:

javascript
function reducer(state, action) { return { ...state, user: { ...state.user, name: 'Alex' } } }

Redux Toolkit (Immer inside):

javascript
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

javascript
[current state] (Immer creates a draft via Proxy) [draft: temporary copy] (you "mutate" it) Immer records the changes [new immutable state]

Summary

WhatHow it works
ImmerA library that creates immutable copies via Proxy
RTKUses Immer inside createSlice()
DeveloperWrites "mutating" code but gets immutability for free
ResultLess 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.