NgRx Toolkit v21
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:
| Feature | Description |
|---|---|
withDevtools() | Allows any SignalStore (Redux‑based or not) to use the Redux DevTools. Add withDevtools('storeName') to visualize the store state. |
@ngrx/signals/event & withFeature | Now 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 extensions | See the full list in the documentation. |
v20 Minor Features: withResource, withEntityResources, & Mutations
withResource() / withEntityResources()
withResourceconnects 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.withEntityResourcesprovides 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
- Improved error handling for
withResource()andwithEntityResources() - Events integration into the Redux DevTools
clearUndoRedointroduced (replacesstore.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
| Value | Description |
|---|---|
'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
-
Replace usages of
withReducerwithwithTrackedReducer.
(NativewithReducersupport is planned but requires upstream changes in@ngrx/signals.) -
Specify
withGlitchTrackinginsidewithDevtools.
IfwithTrackedReduceris 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
clearStackis deprecated.- Use the new standalone function
clearUndoRedoinstead.
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. 🙏