NgRx, a platform which self-describes as focusing on reactive extensions for Angular based on RxJS, shipped its 10th major iteration with a new component store package for local state management. The new package complements the NgRx Store used for application-wide state management. Additionally, a new experimental NgRx Component package strives to support developers in writing Angular Zoneless applications leveraging Angular Ivy capabilities for better performance.
Brandon Roberts explained in NgRx’s release note the value added by the new local component store:
NgRx Store is about managing complex state from many different sources at a global application level. In some cases, you still want to manage state in a consistent way locally, and don’t need the indirection of actions, reducers, and effects. We’ve designed a new package to handle state at a local level while providing the same benefits similar to the NgRx Store.
NgRx applications are architected around four key concepts that are reminiscent of the Redux or the Elm architecture. Actions capture events, whether originating from the user or from other interfaced systems (e.g. sockets, or REST server):
import { createAction, props } from '@ngrx/store';
export const login = createAction(
'[Login Page] Login',
props<{ username: string; password: string }>()
);
Event handlers typically dispatch user-triggered actions (login
in the previous code sample):
onSubmit(username: string, password: string) {
store.dispatch(login({ username: username, password: password }));
}
The application-wide NgRx store processes the dispatched actions by updating its content (i.e. the application-wide state) and/or scheduling NgRx effects. This behavior is consistent with the fundamental equation of reactive systems, as popularized by the Elm platform and language.
Having a top-level state container may help developers to create maintainable, explicit applications where all state updates can be traced back to a triggering event. This is as a matter of fact the promise of Redux.
However, in many cases, and especially in applications which feature a lot of small components with a fairly limited scope, developers are using components which are the only one to read and update a specific piece of state. That piece of state is typically tied to the life-cycle of a particular component and is obsolete when that component is destroyed. The new NgRx component store thus lets developers create components that handle locally such pieces of state — local state.
The release note explained:
ComponentStore is a stand-alone library that helps to manage local/component state. It’s an alternative to reactive push-based “Service with a Subject” approach.
Component stores can be initialized directly when defining the store:
export interface MoviesState {
movies: Movie[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: []});
}
}
Component stores can also be initialized when the component is created, with the setState
method:
@Component({
template: `
<li *ngFor="let movie of (movies$ | async)">
{{ movie.name }}
</li>
`,
providers: [ComponentStore],
})
export class MoviesPageComponent {
readonly movies$ = this.componentStore.state$.pipe(
map(state => state.movies),
);
constructor(
private readonly componentStore: ComponentStore<{movies: Movie[]}>
) {}
ngOnInit() {
this.componentStore.setState({movies: []});
}
}
The component store provides a select
and updater
methods to read a piece of local state, that works in ways that are analog to functional lenses. The setState
method is also exposed to update and replace the whole local state. Lastly, an effect
method is available to run side effects within an observable pipeline:
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor(private readonly moviesService: MoviesService) {
super({movies: []});
}
// Each new call of getMovie(id) pushed that id into movieId$ stream.
readonly getMovie = this.effect((movieId$: Observable<string>) => {
return movieId$.pipe(
// Handle race condition with the proper choice of the flattening operator.
switchMap((id) => this.moviesService.fetchMovie(id).pipe(
// Act on the result within inner pipe.
tap({
next: (movie) => this.addMovie(movie),
error: (e) => this.logError(e),
}),
// Handle potential error within inner pipe.
catchError(() => EMPTY),
))
);
})
readonly addMovie = this.updater((state, movie: Movie) => ({
movies: [...state.movies, movie],
}));
selectMovie(movieId: string) {
return this.select((state) => state.movies.find(m => m.id === movieId));
}
}
The new component store can be installed with npm
:
npm install @ngrx/component-store --save
or with Angular CLI:
ng add @ngrx/component-store@latest
NgRx 10 also ships with an experimental NgRx Component package. The release note explains:
As we move forward in this new world with Ivy, we want to rethink how we handle reactivity in Angular Zoneless applications. The async pipe set a standard for Angular applications for handling observables and promises in the template. NgRx Component goes further to help you build fully reactive applications with helpers for rendering using observables in the template. NgRx Component puts us on a path for looking at new ways to build reactive Angular applications.
The experimental package comes with two directives *ngrxLet
and *ngrxPush
that are documented separately.
Angular Zoneless applications are handling change detection without resorting toAngular Zones mechanism, possibly boosting runtime performance. The new possibilities are open by Angular Ivy and ahead-of-time compilation.
NgRx is open source software distributed under the MIT license. Contributions are welcome and should follow the contributing guidelines.