From 20649eb23b86290d02b039950a9a567bb25fc543 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Wed, 11 Sep 2024 17:26:05 +0200 Subject: [PATCH 1/5] docs(effects): update examples to standalone api --- .../ngrx.io/content/guide/effects/index.md | 59 +++++-------------- .../content/guide/effects/lifecycle.md | 1 + 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 83ff0ab51c..c70e167ce2 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -23,13 +23,20 @@ In a service-based application, your components interact with data through many Imagine that your application manages movies. Here is a component that fetches and displays a list of movies. + + +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; + @Component({ template: ` <li *ngFor="let movie of movies"> {{ movie.name }} </li> ` + standalone: true, + imports: [CommonModule], }) export class MoviesPageComponent { movies: Movie[]; @@ -68,12 +75,18 @@ The component has multiple responsibilities: Effects handle external data and interactions, allowing your services to be less stateful and only perform tasks related to external interactions. Next, refactor the component to put the shared movie data in the `Store`. Effects handle the fetching of movie data. + +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; + @Component({ template: ` <div *ngFor="let movie of movies$ | async"> {{ movie.name }} </div> - ` + `, + standalone: true, + imports: [CommonModule], }) export class MoviesPageComponent { movies$: Observable<Movie[]> = this.store.select(state => state.movies); @@ -229,31 +242,6 @@ It's recommended to inject all dependencies as effect function arguments for eas ## Registering Root Effects -After you've written class-based or functional effects, you must register them so the effects start running. To register root-level effects, add the `EffectsModule.forRoot()` method with an array or sequence of effects classes and/or functional effects dictionaries to your `AppModule`. - - -import { NgModule } from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -@NgModule({ - imports: [ - EffectsModule.forRoot(MoviesEffects, actorsEffects), - ], -}) -export class AppModule {} - - -
- -The `EffectsModule.forRoot()` method must be added to your `AppModule` imports even if you don't register any root-level effects. - -
- -### Using the Standalone API - Registering effects can also be done using the standalone APIs if you are bootstrapping an Angular application using standalone features. @@ -281,25 +269,6 @@ Effects start running **immediately** after instantiation to ensure they are lis ## Registering Feature Effects -For feature modules, register your effects by adding the `EffectsModule.forFeature()` method in the `imports` array of your `NgModule`. - - -import { NgModule } from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -@NgModule({ - imports: [ - EffectsModule.forFeature(MoviesEffects, actorsEffects) - ], -}) -export class MovieModule {} - - -### Using the Standalone API - Feature-level effects are registered in the `providers` array of the route config. The same `provideEffects()` function is used in root-level and feature-level effects. diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index 957d205adf..bd5a76c56f 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -129,6 +129,7 @@ export function effectResubscriptionHandler>T extends Action<( ); } +- im not sure what should be done with that one @NgModule({ imports: [EffectsModule.forRoot([MoviesEffects])], providers: [ From a9b4bcd05cc4cdf9cbf718e7c381922f39d96ac5 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Fri, 13 Sep 2024 17:18:26 +0200 Subject: [PATCH 2/5] docs(effects): add line breaks for better readability, remove constructors, add missing imports --- .../ngrx.io/content/guide/effects/index.md | 136 +++++++++++------- 1 file changed, 86 insertions(+), 50 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index c70e167ce2..8871b3d341 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -1,10 +1,16 @@ # @ngrx/effects -Effects are an RxJS powered side effect model for [Store](guide/store). Effects use streams to provide [new sources](https://martinfowler.com/eaaDev/EventSourcing.html) of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events. +Effects are an RxJS powered side effect model for [Store](guide/store). +Effects use streams to provide [new sources](https://martinfowler.com/eaaDev/EventSourcing.html) +of actions to reduce state based on external interactions +such as network requests, web socket messages and time-based events. ## Introduction -In a service-based Angular application, components are responsible for interacting with external resources directly through services. Instead, effects provide a way to interact with those services and isolate them from the components. Effects are where you handle tasks such as fetching data, long-running tasks that produce multiple events, and other external interactions where your components don't need explicit knowledge of these interactions. +In a service-based Angular application, components are responsible for interacting with external resources directly through services. +Instead, effects provide a way to interact with those services and isolate them from the components. +Effects are where you handle tasks such as fetching data, long-running tasks that produce multiple events, +and other external interactions where your components don't need explicit knowledge of these interactions. ## Key Concepts @@ -19,15 +25,18 @@ Detailed installation instructions can be found on the [Installation](guide/effe ## Comparison with Component-Based Side Effects -In a service-based application, your components interact with data through many different services that expose data through properties and methods. These services may depend on other services that manage other sets of data. Your components consume these services to perform tasks, giving your components many responsibilities. +In a service-based application, your components interact with data through many +different services that expose data through properties and methods. +These services may depend on other services that manage other sets of data. +Your components consume these services to perform tasks, giving your components many responsibilities. Imagine that your application manages movies. Here is a component that fetches and displays a list of movies. - import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ template: ` @@ -36,15 +45,16 @@ import { CommonModule } from '@angular/common'; </li> ` standalone: true, - imports: [CommonModule], + imports: [CommonModule, MoviesService], }) export class MoviesPageComponent { + private moviesService: inject(MoviesService); movies: Movie[]; - constructor(private movieService: MoviesService) {} - ngOnInit() { - this.movieService.getAll().subscribe(movies => this.movies = movies); + this.movieService.getAll() + .pipe(takeUntilDestroyed()) + .subscribe(movies => this.movies = movies); } } @@ -53,10 +63,13 @@ You also have the corresponding service that handles the fetching of movies. @Injectable({ - providedIn: 'root' + providedIn: 'root', + standalone: true, + imports: [HttpClient], + }) export class MoviesService { - constructor (private http: HttpClient) {} + private http: inject(HttpClient); getAll() { return this.http.get('/movies'); @@ -70,9 +83,13 @@ The component has multiple responsibilities: - Using the service to perform a _side effect_, reaching out to an external API to fetch the movies. - Changing the _state_ of the movies within the component. -`Effects` when used along with `Store`, decrease the responsibility of the component. In a larger application, this becomes more important because you have multiple sources of data, with multiple services required to fetch those pieces of data, and services potentially relying on other services. +`Effects` when used along with `Store`, decrease the responsibility of the component. +In a larger application, this becomes more important because you have multiple sources of data, +with multiple services required to fetch those pieces of data, and services potentially relying on other services. -Effects handle external data and interactions, allowing your services to be less stateful and only perform tasks related to external interactions. Next, refactor the component to put the shared movie data in the `Store`. Effects handle the fetching of movie data. +Effects handle external data and interactions, allowing your services to be less stateful +and only perform tasks related to external interactions. +Next, refactor the component to put the shared movie data in the `Store`. Effects handle the fetching of movie data. @@ -88,7 +105,8 @@ import { CommonModule } from '@angular/common'; standalone: true, imports: [CommonModule], }) -export class MoviesPageComponent { +export class MoviesPageComponent implements OnInit{ + private store = inject(Store<{ movies: Movie[] }>) movies$: Observable<Movie[]> = this.store.select(state => state.movies); constructor(private store: Store<{ movies: Movie[] }>) {} @@ -99,7 +117,10 @@ export class MoviesPageComponent { } -The movies are still fetched through the `MoviesService`, but the component is no longer concerned with how the movies are fetched and loaded. It's only responsible for declaring its _intent_ to load movies and using selectors to access movie list data. Effects are where the asynchronous activity of fetching movies happens. Your component becomes easier to test and less responsible for the data it needs. +The movies are still fetched through the `MoviesService`, but the component is no longer concerned with how the movies are fetched and loaded. +It's only responsible for declaring its _intent_ to load movies and using selectors to access movie list data. +Effects are where the asynchronous activity of fetching movies happens. +Your component becomes easier to test and less responsible for the data it needs. ## Writing Effects @@ -158,10 +179,15 @@ The `loadMovies$` effect is listening for all dispatched actions through the `Ac ## Handling Errors -Effects are built on top of observable streams provided by RxJS. Effects are listeners of observable streams that continue until an error or completion occurs. In order for effects to continue running in the event of an error in the observable, or completion of the observable stream, they must be nested within a "flattening" operator, such as `mergeMap`, `concatMap`, `exhaustMap` and other flattening operators. The example below shows the `loadMovies$` effect handling errors when fetching movies. +Effects are built on top of observable streams provided by RxJS. +Effects are listeners of observable streams that continue until an error or completion occurs. +In order for effects to continue running in the event of an error in the observable, +or completion of the observable stream, they must be nested within a "flattening" operator, +such as `mergeMap`, `concatMap`, `exhaustMap` and other flattening operators. +The example below shows the `loadMovies$` effect handling errors when fetching movies. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { map, exhaustMap, catchError } from 'rxjs/operators'; @@ -169,6 +195,8 @@ import { MoviesService } from './movies.service'; @Injectable() export class MoviesEffects { + private actions$ = inject(Actions); + private moviesService = inject(MoviesService); loadMovies$ = createEffect(() => this.actions$.pipe( @@ -181,21 +209,20 @@ export class MoviesEffects { ) ) ); - - constructor( - private actions$: Actions, - private moviesService: MoviesService - ) {} } -The `loadMovies$` effect returns a new observable in case an error occurs while fetching movies. The inner observable handles any errors or completions and returns a new observable so that the outer stream does not die. You still use the `catchError` operator to handle error events, but return an observable of a new action that is dispatched to the `Store`. +The `loadMovies$` effect returns a new observable in case an error occurs while fetching movies. +The inner observable handles any errors or completions and returns a new observable so that the outer stream does not die. +You still use the `catchError` operator to handle error events, but return an observable of a new action that is dispatched to the `Store`. ## Functional Effects -Functional effects are also created by using the `createEffect` function. They provide the ability to create effects outside the effect classes. +Functional effects are also created by using the `createEffect` function. +They provide the ability to create effects outside the effect classes. -To create a functional effect, add the `functional: true` flag to the effect config. Then, to inject services into the effect, use the [`inject` function](https://angular.dev/api/core/inject). +To create a functional effect, add the `functional: true` flag to the effect config. +Then, to inject services into the effect, use the [`inject` function](https://angular.dev/api/core/inject). import { inject } from '@angular/core'; @@ -236,7 +263,9 @@ export const displayErrorAlert = createEffect(
-It's recommended to inject all dependencies as effect function arguments for easier testing. However, it's also possible to inject dependencies in the effect function body. In that case, the [`inject` function](https://angular.dev/api/core/inject) must be called within the synchronous context. +It's recommended to inject all dependencies as effect function arguments for easier testing. +However, it's also possible to inject dependencies in the effect function body. +In that case, the [`inject` function](https://angular.dev/api/core/inject) must be called within the synchronous context.
@@ -263,7 +292,8 @@ bootstrapApplication(AppComponent, {
-Effects start running **immediately** after instantiation to ensure they are listening for all relevant actions as soon as possible. Services used in root-level effects are **not** recommended to be used with services that are used with the `APP_INITIALIZER` token. +Effects start running **immediately** after instantiation to ensure they are listening for all relevant actions as soon as possible. +Services used in root-level effects are **not** recommended to be used with services that are used with the `APP_INITIALIZER` token.
@@ -290,7 +320,10 @@ export const routes: Route[] = [
-**Note:** Registering an effects class multiple times, either by `forRoot()`, `forFeature()`, or `provideEffects()`, (for example in different lazy loaded features) will not cause the effects to run multiple times. There is no functional difference between effects loaded by `root` and `feature`; the important difference between the functions is that `root` providers sets up the providers required for effects. +**Note:** Registering an effects class multiple times, either by `forRoot()`, `forFeature()`, or `provideEffects()`, +(for example in different lazy loaded features) will not cause the effects to run multiple times. +There is no functional difference between effects loaded by `root` and `feature`; the important difference between the functions +is that `root` providers sets up the providers required for effects.
@@ -311,7 +344,8 @@ providers: [
-The `EffectsModule.forFeature()` method or `provideEffects()` function must be added to the module imports/route config even if you only provide effects over token, and don't pass them through parameters. (Same goes for `EffectsModule.forRoot()`) +The `EffectsModule.forFeature()` method or `provideEffects()` function must be added to the module imports/route config +even if you only provide effects over token, and don't pass them through parameters. (Same goes for `EffectsModule.forRoot()`)
@@ -319,7 +353,11 @@ The `EffectsModule.forFeature()` method or `provideEffects()` function must be a If you have a module-based Angular application, you can still use standalone components. NgRx standalone APIs support this workflow as well. -For module-based apps, you have the `EffectsModule.forRoot([...])` included in the `imports` array of your `AppModule`, which registers the root effects for dependency injection. For a standalone component with feature state/effects registered in its route configuration to successfully run effects, you will need to use the `provideEffects([...])` function in the `providers` array of your `AppModule` to register the injection token. For module-based with standalone components, you will simply have both. +For module-based apps, you have the `EffectsModule.forRoot([...])` included in the `imports` array of your `AppModule`, +which registers the root effects for dependency injection. +For a standalone component with feature state/effects registered in its route configuration to successfully run effects, +you will need to use the `provideEffects([...])` function in the `providers` array of your `AppModule` to register the injection token. +For module-based with standalone components, you will simply have both. import { NgModule } from '@angular/core'; @@ -342,7 +380,8 @@ export class AppModule {} ## Incorporating State -If additional metadata is needed to perform an effect besides the initiating action's `type`, we should rely on passed metadata from an action creator's `props` method. +If additional metadata is needed to perform an effect besides the initiating action's `type`, +we should rely on passed metadata from an action creator's `props` method. Let's look at an example of an action initiating a login request using an effect with additional passed metadata: @@ -357,7 +396,7 @@ export const login = createAction( -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, exhaustMap, map } from 'rxjs/operators'; @@ -370,6 +409,9 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { + private actions$ = inject(Actions); + private authService = inject(AuthService); + login$ = createEffect(() => this.actions$.pipe( ofType(LoginPageActions.login), @@ -381,22 +423,18 @@ export class AuthEffects { ) ) ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} } The `login` action has additional `credentials` metadata which is passed to a service to log the specific user into the application. -However, there may be cases when the required metadata is only accessible from state. When state is needed, the RxJS `withLatestFrom` or the @ngrx/effects `concatLatestFrom` operators can be used to provide it. +However, there may be cases when the required metadata is only accessible from state. +When state is needed, the RxJS `withLatestFrom` or the @ngrx/effects `concatLatestFrom` operators can be used to provide it. The example below shows the `addBookToCollectionSuccess$` effect displaying a different alert depending on the number of books in the collection state. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { Actions, ofType, createEffect, concatLatestFrom } from '@ngrx/effects'; import { tap } from 'rxjs/operators'; @@ -405,6 +443,9 @@ import * as fromBooks from '../reducers'; @Injectable() export class CollectionEffects { + private actions$ = inject(Actions); + private store = inject(Store) + addBookToCollectionSuccess$ = createEffect( () => this.actions$.pipe( @@ -420,11 +461,6 @@ export class CollectionEffects { ), { dispatch: false } ); - - constructor( - private actions$: Actions, - private store: Store<fromBooks.State> - ) {} } @@ -438,12 +474,14 @@ To learn about testing effects that incorporate state, see the [Effects that use ## Using Other Observable Sources for Effects -Because effects are merely consumers of observables, they can be used without actions and the `ofType` operator. This is useful for effects that don't need to listen to some specific actions, but rather to some other observable source. +Because effects are merely consumers of observables, they can be used without actions and the `ofType` operator. +This is useful for effects that don't need to listen to some specific actions, but rather to some other observable source. -For example, imagine we want to track click events and send that data to our monitoring server. This can be done by creating an effect that listens to the `document` `click` event and emits the event data to our server. +For example, imagine we want to track click events and send that data to our monitoring server. +This can be done by creating an effect that listens to the `document` `click` event and emits the event data to our server. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, fromEvent } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { createEffect } from '@ngrx/effects'; @@ -452,14 +490,12 @@ import { UserActivityService } from '../services/user-activity.service'; @Injectable() export class UserActivityEffects { + private userActivityService: UserActivityService = inject(UserActivityService); + trackUserActivity$ = createEffect(() => fromEvent(document, 'click').pipe( concatMap(event => this.userActivityService.trackUserActivity(event)), ), { dispatch: false } ); - - constructor( - private userActivityService: UserActivityService, - ) {} } From f05d500fe9eed87d50c08dfad9f8b2f3396f7f9b Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Wed, 9 Oct 2024 16:25:21 +0200 Subject: [PATCH 3/5] docs(effects): change module, to bootstrap application --- .../content/guide/effects/lifecycle.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index bd5a76c56f..8a246e5c40 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -129,20 +129,21 @@ export function effectResubscriptionHandler>T extends Action<( ); } -- im not sure what should be done with that one -@NgModule({ - imports: [EffectsModule.forRoot([MoviesEffects])], - providers: [ - { - provide: EFFECTS_ERROR_HANDLER, - useValue: effectResubscriptionHandler, - }, - { - provide: ErrorHandler, - useClass: CustomErrorHandler +bootstrapApplication( + AppComponent, + { + providers: [ + { + provide: EFFECTS_ERROR_HANDLER, + useValue: effectResubscriptionHandler, + }, + { + provide: ErrorHandler, + useClass: CustomErrorHandler + } + ], } - ], -}) +)
## Controlling Effects From 6a49f4c7fbdbd4cbd7350c336892d3313d253906 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Wed, 9 Oct 2024 16:28:42 +0200 Subject: [PATCH 4/5] docs(effects): merge effect registration, delete standalone mentions --- .../ngrx.io/content/guide/effects/index.md | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 8871b3d341..957e03da14 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -269,9 +269,10 @@ In that case, the [`inject` function](https://angular.dev/api/core/inject) must -## Registering Root Effects +## Registering Effects -Registering effects can also be done using the standalone APIs if you are bootstrapping an Angular application using standalone features. +Feature-level effects are registered in the `providers` array of the route config. +The same `provideEffects()` function is used in root-level and feature-level effects. import { bootstrapApplication } from '@angular/platform-browser'; @@ -297,27 +298,6 @@ Services used in root-level effects are **not** recommended to be used with serv -## Registering Feature Effects - -Feature-level effects are registered in the `providers` array of the route config. The same `provideEffects()` function is used in root-level and feature-level effects. - - -import { Route } from '@angular/router'; -import { provideEffects } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -export const routes: Route[] = [ - { - path: 'movies', - providers: [ - provideEffects(MoviesEffects, actorsEffects) - ] - } -]; - -
**Note:** Registering an effects class multiple times, either by `forRoot()`, `forFeature()`, or `provideEffects()`, From 656ca3fa4f8f8fbb8ddc6490bbe7898a64eda6cd Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Wed, 9 Oct 2024 16:30:00 +0200 Subject: [PATCH 5/5] docs(effects): add module based application note --- projects/ngrx.io/content/guide/effects/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 957e03da14..4937d74124 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -296,6 +296,8 @@ bootstrapApplication(AppComponent, { Effects start running **immediately** after instantiation to ensure they are listening for all relevant actions as soon as possible. Services used in root-level effects are **not** recommended to be used with services that are used with the `APP_INITIALIZER` token. +An example of the `@ngrx/router-store` setup in module-based applications is available at the [following link](https://v17.ngrx.io/guide/router-store#setup). +