NgRx Toolkit v21

Published: (January 19, 2026 at 12:20 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

What the Toolkit Provides

The NgRx Toolkit is a rich set of extensions you typically need in Angular applications. Below is a brief history of its core functionality:

FeatureDescription
withDevtools()Allows any SignalStore (Redux‑based or not) to use the Redux DevTools. Add withDevtools('storeName') to visualize the store state.
@ngrx/signals/event & withFeatureNow part of @ngrx/signals core; originally incubated in the Toolkit.
withStorageSync()Synchronizes state with Web Storage (localStorage / sessionStorage) and IndexedDB (via an async strategy). Use withStorageSync('storeName'). IndexedDB support was added last year by a community contribution (#134) from GitHub user mzkmnk.
Other extensionsSee the full list in the documentation.

NgRx Toolkit extensions


v20 Minor Features: withResource, withEntityResources, & Mutations

withResource() / withEntityResources()

  • withResource connects Angular’s Resource API with the store, allowing a store to manage async data (e.g., loading from an API). It supports both unnamed and named variants.
  • withEntityResources provides the same functionality for stores built with @ngrx/signals/entities.

Mutations API

The Mutations API adds the “write” side of a typical REST experience. It offers:

  • Stand‑alone functions: httpMutation, rxMutation
  • A feature wrapper: withMutations

The design was inspired by Angular Query and Marko Stanimirović’s proposed mutations API, with internal discussions involving Alex Rickabaugh.

import {
  httpMutation,
  rxMutation,
  withMutations,
  withResource,
  withEntityResources,
} from '@angular-architects/ngrx-toolkit';

export const UserStore = signalStore(
  withState({ userId: undefined as number | undefined }),

  // Async data handling via Angular Resource API
  withResource(({ userId }) => ({
    detail: httpResource(() =>
      userId === undefined ? undefined : `/user/${userId}`
    ),
  })),

  // Mutations (save, post, etc.)
  withMutations((store, userService = inject(UserService)) => ({
    saveUserDetail: rxMutation({
      operation: (params: Params) =>
        userService.saveUserDetail(store.counter(), params.value),
      onSuccess: (result) => {
        // …
      },
      onError: (error) => {
        // …
      },
    }),

    saveToServer: httpMutation({
      request: () => ({
        url: `https://httpbin.org/post`,
        method: 'POST',
        body: { counter: store.counter() },
      }),
      parse: (response) => response as UserResponse,
      onSuccess: (result) => {
        // …
      },
      onError: (error) => {
        // …
      },
    }),
  })),

  // Entity‑level resources
  withEntityResources(() =>
    resource({
      loader: () => Promise.resolve([] as User[]),
      defaultValue: [],
    })
  )
);

v21 – What’s New

Major Additions

  1. Improved error handling for withResource() and withEntityResources()
  2. Events integration into the Redux DevTools
  3. clearUndoRedo introduced (replaces store.clearStack)

Upgraded withResource() / withEntityResources() Error Handling

Angular resources can enter a dead‑lock scenario: once a resource is in an error state, updating a signal in params triggers patchState, which again accesses the state value, potentially causing another error.

The Toolkit now offers several strategies for handling errors in withResource():

type ErrorHandlingStrategy =
  | 'throw'    // Re‑throw the original error (default)
  | 'ignore'   // Silently ignore the error and keep the previous state
  | 'fallback' // Provide a fallback value via a user‑supplied function
  | 'custom'   // Execute a custom error‑handler callback

Select the desired strategy when configuring the resource:

withResource(
  ({ userId }) => ({
    detail: httpResource(() =>
      userId === undefined ? undefined : `/user/${userId}`
    ),
  }),
  {
    errorHandling: 'fallback',
    fallbackValue: () => ({ name: 'Anonymous', id: 0 })
  }
);

Events Integration into DevTools

withDevtools() now captures custom events emitted from stores, allowing you to see them alongside state changes in the Redux DevTools UI.

clearUndoRedo

The previous store.clearStack() method has been superseded by clearUndoRedo(), which more clearly conveys its purpose of clearing both undo and redo histories.


References

  • Documentation: (link to official docs)
  • IndexedDB contribution: (by mzkmnk)
  • Angular Query (mutations inspiration): (link)
  • Marko Stanimirović’s mutations proposal: (link)

Error‑Handling Strategies

errorHandling = 'native' | 'undefined value' | 'previous value';

withResource(
  (store) => {
    const resolver = inject(AddressResolver);
    return {
      address: resource({
        params: store.id,
        loader: ({ params: id }) => resolver.resolve(id),
      }),
    };
  },
  // Other values: 'native' and 'previous value'
  { errorHandling: 'undefined value' } // default if not specified
);

Options

ValueDescription
'undefined value' (default)When an error occurs, the resource’s value becomes undefined.
'previous value'If the resource previously held a value, that value is returned; otherwise an error is thrown.
'native'No special handling – the default error behaviour is used.

For withEntityResources(), the strategy is 'undefined value'.

Under the hood, 'previous value' and 'undefined value' proxy the value. For a detailed explanation, see the JSDoc for the error‑handling strategy.


Events Integration into DevTools

There’s a bit of irony here: the NgRx Toolkit introduced events to the Signal Store before an official plugin existed, and it also provides Redux DevTools integration (with or without Redux). However, the now‑official NgRx events feature didn’t map directly to the Toolkit’s withDevtools.

In NgRx Toolkit v21 we fix this with withTrackedReducer(), an alternative way to track reducer‑based state changes in Redux DevTools.

How to Use It

  1. Replace usages of withReducer with withTrackedReducer.
    (Native withReducer support is planned but requires upstream changes in @ngrx/signals.)

  2. Specify withGlitchTracking inside withDevtools.
    If withTrackedReducer is used without DevTools and glitch tracking, runtime errors will be thrown.

import {
  withTrackedReducer,
  withGlitchTracking,
  withDevtools,
} from '@angular-architects/ngrx-toolkit';

export const bookEvents = eventGroup({
  source: 'Book Store',
  events: {
    loadBooks: type(),
  },
});

const Store = signalStore(
  { providedIn: 'root' },
  withDevtools('book-store-events', withGlitchTracking()),
  withState({
    books: [] as Book[],
  }),
  withTrackedReducer(
    // `[Book Store] loadBooks` will appear in the DevTools
    on(bookEvents.loadBooks, () => ({
      books: mockBooks,
    }))
  ),
  withHooks({
    onInit() {
      injectDispatch(bookEvents).loadBooks();
    },
  })
);

clearUndoRedo – Replacing store.clearStack

  • clearStack is deprecated.
  • Use the new standalone function clearUndoRedo instead.

clearUndoRedo performs a soft reset (does not set the state to null) by default. A hard reset can be requested via options:

clearUndoRedo(store, { lastRecord: null });

Implemented by Gregor Woiwode. Back‑ported to NgRx Toolkit v19.5.0 and v20.7.0.


ngrx-toolkit-openapi-gen

We received a fantastic Christmas present from Murat Sari: an OpenAPI generator that creates:

  • ✅ an NgRx Signal Store
  • ✅ Resources
  • ✅ Mutations
  • ✅ Code based on a Zod schema

The generated code is genuinely beautiful—something rarely seen in code generators.

  • npm: (link)
  • Documentation: (link)

Future Release Strategy for Compatible Angular Versions

Version 21 of the Toolkit took longer due to new features and fixes. NgRx v21 was compatible with later Toolkit v20 releases, but only with certain overrides. To smooth the experience, we released NgRx Toolkit v20.6.0, supporting both NgRx v20 and v21. The same applies to v20.7.0, which back‑ported several v21 features.

Going forward:
If obstacles arise for any upcoming major Toolkit release, we will publish a minor Toolkit version that is compatible with the next stable major NgRx release as soon as it’s ready for integration.


Thank You!

We are grateful to everyone who contributes time, expertise, discussions, and code. Special thanks to the highlighted contributors of this article:

Your efforts make the NgRx Toolkit community stronger. 🙏

Back to Blog

Related posts

Read more »