NgRx is a widely adopted state management solution for Angular. It offers a centralized store to hold the application's state and a suite of utilities to manage state changes in a predictable sequence. In addition, NgRx includes a robust set of reducers—pure functions that define how the state should transform in response to dispatched actions.
A fundamental principle in NgRx is immutability; the state must never be altered in place. When an action arrives, the corresponding reducer generates a fresh copy of the state, applying the necessary modifications to this new version. This approach ensures the state remains consistent and predictable, simplifying testing and debugging considerably.
However, crafting these immutable state copies can turn into a tedious chore, especially when dealing with deeply nested structures. That is precisely where ngrx-immer comes into play, offering a more streamlined method for managing immutability within NgRx reducers.
Immer

Comparing NgRx on and immerOn

Imagine a state object that holds a collection of users, each with a name and an age. We need three operations: add a new user, rename an existing one, and remove a user from the collection.

With the standard on method, the NgRx reducer would look like this:

import { createReducer, on } from '@ngrx/store';
import { addUser, updateUser, deleteUser } from './user.actions';

export interface User {
  name: string;
  age: number;
}

export const initialState: User[] = [];

export const _userReducer = createReducer(
  initialState,
  on(addUser, (state, { user }) => [...state, user]),
  on(updateUser, (state, { user, name }) =>
    state.map(u => (u.name === user.name ? { ...u, name } : u))
  ),
  on(deleteUser, (state, { user }) =>
    state.filter(u => u.name !== user.name)
  )
);

Switching to the immerOn method provided by ngrx-immer, the same reducer becomes:

import { createReducer, immerOn } from 'ngrx-immer';
import { addUser, updateUser, deleteUser } from './user.actions';

export interface User {
  name: string;
  age: number;
}

export const initialState: User[] = [];

export const _userReducer = createReducer(
  initialState,
  immerOn(addUser, (state, { user }) => {
    state.push(user);
  }),
  immerOn(updateUser, (state, { user, name }) => {
    const index = state.findIndex(u => u.name === user.name);
    state[index].name = name;
  }),
  immerOn(deleteUser, (state, { user }) => {
    const index = state.findIndex(u => u.name === user.name);
    state.splice(index, 1);
  })
);

The difference is clear: immerOn offers a more concise and straightforward way to modify the state. You work with the state as if it were mutable, avoiding the repetitive spread operator needed to construct new objects.

No Full Rewrite Needed — Blend Both Approaches

You don’t need to migrate every reducer in your application.

The immerOn method is ideal for the most intricate scenarios.

It is perfectly fine to mix immerOn from the ngrx-immer package with the regular on from @ngrx/store in the same project.

import { createReducer, on, immerOn } from 'ngrx-immer';
import { addUser, updateUser, deleteUser, togglePremium } from './user.actions';

export interface User {
  name: string;
  age: number;
  premium: boolean;
}

export const initialState: User[] = [];

export const _userReducer = createReducer(
  initialState,
  on(addUser, (state, { user }) => [...state, user]),
  immerOn(updateUser, (state, { user, name }) => {
    const index = state.findIndex(u => u.name === user.name);
    state[index].name = name;
  }),
  immerOn(deleteUser, (state, { user }) => {
    const index = state.findIndex(u => u.name === user.name);
    state.splice(index, 1);
  }),
  immerOn(togglePremium, (state, { user }) => {
    const index = state.findIndex(u => u.name === user.name);
    state[index].premium = !state[index].premium;
  })
);

Adopting this approach requires barely any effort, yet it dramatically enhances the clarity and maintainability of your codebase.

rxjs

Should This Be Integrated into the Official NgRx?

In my view, this library deserves a place within the core NgRx package.

What’s the reasoning?

Relying on external packages maintained outside the core ecosystem always carries the risk that development may stall or cease entirely.

A smaller project with fewer contributors poses a higher likelihood of abandonment.

Given the utility of this feature, bundling it with NgRx would be a sensible move.

Have a different perspective? Let’s talk about it.

References

Full recognition goes to Tim Deschryver, the creator of this library.

https://github.com/timdeschryver/ngrx-immer