From 20649eb23b86290d02b039950a9a567bb25fc543 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Wed, 11 Sep 2024 17:26:05 +0200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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). +
From 7bab6020c1457594232e49ecbbf5dfcb44cdfea9 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:39:02 +0100 Subject: [PATCH 06/16] Apply suggestions from code review --- .../ngrx.io/content/guide/effects/index.md | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 4937d74124..17b54dc46f 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -36,7 +36,6 @@ Imagine that your application manages movies. Here is a component that fetches a import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ template: ` @@ -48,12 +47,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; imports: [CommonModule, MoviesService], }) export class MoviesPageComponent { - private moviesService: inject(MoviesService); + private moviesService = inject(MoviesService); movies: Movie[]; ngOnInit() { this.movieService.getAll() - .pipe(takeUntilDestroyed()) .subscribe(movies => this.movies = movies); } } @@ -69,7 +67,7 @@ You also have the corresponding service that handles the fetching of movies. }) export class MoviesService { - private http: inject(HttpClient); + private http = inject(HttpClient); getAll() { return this.http.get('/movies'); @@ -106,7 +104,7 @@ import { CommonModule } from '@angular/common'; imports: [CommonModule], }) export class MoviesPageComponent implements OnInit{ - private store = inject(Store<{ movies: Movie[] }>) + private store = inject(Store<{ movies: Movie[] }>) movies$: Observable<Movie[]> = this.store.select(state => state.movies); constructor(private store: Store<{ movies: Movie[] }>) {} @@ -296,16 +294,12 @@ 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).
-**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 (for example in different lazy loaded features) does not cause the effects to run multiple times.
@@ -326,8 +320,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 `provideEffects()` method must be added to the module imports/route config +even if you only provide effects over token, and don't pass them through parameters.
@@ -426,7 +420,7 @@ import * as fromBooks from '../reducers'; @Injectable() export class CollectionEffects { private actions$ = inject(Actions); - private store = inject(Store) + private store = inject(Store<fromBooks.State>) addBookToCollectionSuccess$ = createEffect( () => @@ -472,7 +466,7 @@ import { UserActivityService } from '../services/user-activity.service'; @Injectable() export class UserActivityEffects { - private userActivityService: UserActivityService = inject(UserActivityService); + private userActivityService = inject(UserActivityService); trackUserActivity$ = createEffect(() => fromEvent(document, 'click').pipe( From afa058e89b1c7233785d81d6df461418d1028f83 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:17:25 +0100 Subject: [PATCH 07/16] remove module references --- .../ngrx.io/content/guide/effects/index.md | 93 ++++++------------- 1 file changed, 27 insertions(+), 66 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 17b54dc46f..f28bf8a454 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -1,7 +1,7 @@ # @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) +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. @@ -25,7 +25,7 @@ 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 +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. @@ -33,7 +33,6 @@ Your components consume these services to perform tasks, giving your components 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'; @@ -64,7 +63,6 @@ You also have the corresponding service that handles the fetching of movies. providedIn: 'root', standalone: true, imports: [HttpClient], - }) export class MoviesService { private http = inject(HttpClient); @@ -90,7 +88,6 @@ 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'; @@ -117,7 +114,7 @@ export class MoviesPageComponent implements OnInit{ 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. +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 @@ -132,12 +129,6 @@ Effects are injectable service classes with distinct parts: - Effects are subscribed to the `Store` observable. - Services are injected into effects to interact with external APIs and handle streams. -
- -**Note:** Since NgRx v15.2, classes are not required to create effects. Learn more about functional effects [here](#functional-effects). - -
- To show how you handle loading movies from the example above, let's look at `MoviesEffects`. @@ -178,10 +169,10 @@ 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. +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. +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. @@ -219,7 +210,7 @@ You still use the `catchError` operator to handle error events, but return an ob 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. +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). @@ -269,7 +260,13 @@ In that case, the [`inject` function](https://angular.dev/api/core/inject) must ## Registering Effects -Feature-level effects are registered in the `providers` array of the route config. +
+ +Registering an effects class multiple times (for example in different lazy loaded features) does not cause the effects to run multiple times. + +
+ +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. @@ -294,20 +291,13 @@ 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. - - - -
- -**Note:** Registering an effects class multiple times (for example in different lazy loaded features) does not cause the effects to run multiple times. -
-## Alternative Way of Registering Effects +### Alternative Way of Registering Effects You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`. - + providers: [ MoviesEffects, { @@ -318,45 +308,9 @@ providers: [ ] -
- -The `provideEffects()` method must be added to the module imports/route config -even if you only provide effects over token, and don't pass them through parameters. - -
- -## Standalone API in module-based apps - -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. - - -import { NgModule } from '@angular/core'; -import { EffectsModule, provideEffects } from '@ngrx/effects'; - -import { MoviesEffects } from './effects/movies.effects'; -import * as actorsEffects from './effects/actors.effects'; - -@NgModule({ - imports: [ - EffectsModule.forRoot(MoviesEffects, actorsEffects), - ], - providers: [ - provideEffects(MoviesEffects, actorsEffects) - ] -}) -export class AppModule {} - - - ## Incorporating State -If additional metadata is needed to perform an effect besides the initiating action's `type`, +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: @@ -404,7 +358,7 @@ export class AuthEffects { 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. +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. @@ -442,7 +396,7 @@ export class CollectionEffects {
-**Note:** For performance reasons, use a flattening operator like `concatLatestFrom` to prevent the selector from firing until the correct action is dispatched. +For performance reasons, use a flattening operator like `concatLatestFrom` to prevent the selector from firing until the correct action is dispatched.
@@ -451,7 +405,7 @@ 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. +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. @@ -475,3 +429,10 @@ export class UserActivityEffects { ); }
+ + +
+ +An example of the `@ngrx/effects` in module-based applications is available at the [following link](https://v17.ngrx.io/guide/effects). + +
From 4cb509a235bea981cfbba8eba95d8d706d81fbbf Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:38:41 +0100 Subject: [PATCH 08/16] remove inconsistencies --- .../ngrx.io/content/guide/effects/index.md | 73 +++++++++---------- .../content/guide/effects/lifecycle.md | 48 ++++++------ .../content/guide/effects/operators.md | 18 ++--- 3 files changed, 66 insertions(+), 73 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index f28bf8a454..1715c918c0 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -47,7 +47,7 @@ import { CommonModule } from '@angular/common'; }) export class MoviesPageComponent { private moviesService = inject(MoviesService); - movies: Movie[]; + protected movies: Movie[]; ngOnInit() { this.movieService.getAll() @@ -100,11 +100,9 @@ import { CommonModule } from '@angular/common'; standalone: true, imports: [CommonModule], }) -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[] }>) {} +export class MoviesPageComponent implements OnInit { + private store = inject(Store<{ movies: Movie[] }>); + protected movies$ = this.store.select(state => state.movies); ngOnInit() { this.store.dispatch({ type: '[Movies Page] Load Movies' }); @@ -140,21 +138,19 @@ import { MoviesService } from './movies.service'; @Injectable() export class MoviesEffects { + private actions$ = inject(Actions); + private moviesService = inject(MoviesService); - loadMovies$ = createEffect(() => this.actions$.pipe( - ofType('[Movies Page] Load Movies'), - exhaustMap(() => this.moviesService.getAll() - .pipe( - map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })), - catchError(() => EMPTY) - )) + loadMovies$ = createEffect(() => { + return this.actions$.pipe( + ofType('[Movies Page] Load Movies'), + exhaustMap(() => this.moviesService.getAll() + .pipe( + map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })), + catchError(() => EMPTY) + )); ) - ); - - constructor( - private actions$: Actions, - private moviesService: MoviesService - ) {} + }); }
@@ -185,10 +181,10 @@ import { MoviesService } from './movies.service'; @Injectable() export class MoviesEffects { private actions$ = inject(Actions); - private moviesService = inject(MoviesService); + private moviesService = inject(MoviesService); - loadMovies$ = createEffect(() => - this.actions$.pipe( + loadMovies$ = createEffect(() => { + return this.actions$.pipe( ofType('[Movies Page] Load Movies'), exhaustMap(() => this.moviesService.getAll() .pipe( @@ -196,8 +192,8 @@ export class MoviesEffects { catchError(() => of({ type: '[Movies API] Movies Loaded Error' })) ) ) - ) - ); + ); + }); }
@@ -342,8 +338,8 @@ export class AuthEffects { private actions$ = inject(Actions); private authService = inject(AuthService); - login$ = createEffect(() => - this.actions$.pipe( + login$ = createEffect(() => { + return this.actions$.pipe( ofType(LoginPageActions.login), exhaustMap(action => this.authService.login(action.credentials).pipe( @@ -351,8 +347,8 @@ export class AuthEffects { catchError(error => of(AuthApiActions.loginFailure({ error }))) ) ) - ) - ); + ); + }); }
@@ -374,11 +370,11 @@ import * as fromBooks from '../reducers'; @Injectable() export class CollectionEffects { private actions$ = inject(Actions); - private store = inject(Store<fromBooks.State>) + private store = inject(Store<fromBooks.State>); addBookToCollectionSuccess$ = createEffect( - () => - this.actions$.pipe( + () => { + return this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), concatLatestFrom(action => this.store.select(fromBooks.getCollectionBookIds)), tap(([action, bookCollection]) => { @@ -388,9 +384,9 @@ export class CollectionEffects { window.alert('You have added book number ' + bookCollection.length); } }) - ), - { dispatch: false } - ); + ); + }, + { dispatch: false }); }
@@ -422,15 +418,14 @@ import { UserActivityService } from '../services/user-activity.service'; export class UserActivityEffects { private userActivityService = inject(UserActivityService); - trackUserActivity$ = createEffect(() => - fromEvent(document, 'click').pipe( + trackUserActivity$ = createEffect(() => { + return fromEvent(document, 'click').pipe( concatMap(event => this.userActivityService.trackUserActivity(event)), - ), { dispatch: false } - ); + ); + }, { dispatch: false }); }
-
An example of the `@ngrx/effects` in module-based applications is available at the [following link](https://v17.ngrx.io/guide/effects). diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index 8a246e5c40..9b02ca1960 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -6,12 +6,12 @@ After all the root effects have been added, the root effect dispatches a `ROOT_E You can see this action as a lifecycle hook, which you can use in order to execute some code after all your root effects have been added. -init$ = createEffect(() => - this.actions$.pipe( +init$ = createEffect(() => { + return this.actions$.pipe( ofType(ROOT_EFFECTS_INIT), map(action => ...) - ) -); + ); +}); ## Effect Metadata @@ -23,18 +23,19 @@ Sometimes you don't want effects to dispatch an action, for example when you onl Usage: -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; import { tap } from 'rxjs/operators'; @Injectable() export class LogEffects { - constructor(private actions$: Actions) {} + private actions$ = inject(Actions); - logActions$ = createEffect(() => - this.actions$.pipe( - tap(action => console.log(action)) - ), { dispatch: false }); + logActions$ = createEffect(() => { + return this.actions$.pipe( + tap(action => console.log(action)) + ); + }, { dispatch: false }); } @@ -57,7 +58,7 @@ To disable resubscriptions add `{useEffectsErrorHandler: false}` to the `createE metadata (second argument). -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'; @@ -69,9 +70,12 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { + private actions$ = inject(Actions); + private authService = inject(AuthService); + logins$ = createEffect( - () => - this.actions$.pipe( + () => { + return this.actions$.pipe( ofType(LoginPageActions.login), exhaustMap(action => this.authService.login(action.credentials).pipe( @@ -80,14 +84,10 @@ export class AuthEffects { ) ) // Errors are handled and it is safe to disable resubscription - ), + ); + }, { useEffectsErrorHandler: false } ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} } @@ -183,16 +183,16 @@ import { @Injectable() export class UserEffects implements OnRunEffects { - constructor(private actions$: Actions) {} + private actions$ = inject(Actions); - updateUser$ = createEffect(() => - this.actions$.pipe( + updateUser$ = createEffect(() => { + return this.actions$.pipe( ofType('UPDATE_USER'), tap(action => { console.log(action); }) - ), - { dispatch: false }); + ); + }, { dispatch: false }); ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) { return this.actions$.pipe( diff --git a/projects/ngrx.io/content/guide/effects/operators.md b/projects/ngrx.io/content/guide/effects/operators.md index a7a1dfb6b8..3e1e4d3144 100644 --- a/projects/ngrx.io/content/guide/effects/operators.md +++ b/projects/ngrx.io/content/guide/effects/operators.md @@ -17,7 +17,7 @@ The `ofType` operator takes up to 5 arguments with proper type inference. It can take even more, however the type would be inferred as an `Action` interface. -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'; @@ -30,8 +30,11 @@ import { AuthService } from '../services/auth.service'; @Injectable() export class AuthEffects { - login$ = createEffect(() => - this.actions$.pipe( + private actions$ = inject(Actions); + private authService = inject(AuthService); + + login$ = createEffect(() => { + return this.actions$.pipe( // Filters by Action Creator 'login' ofType(LoginPageActions.login), exhaustMap(action => @@ -40,12 +43,7 @@ export class AuthEffects { catchError(error => of(AuthApiActions.loginFailure({ error }))) ) ) - ) - ); - - constructor( - private actions$: Actions, - private authService: AuthService - ) {} + ); + }); } From f416c7ef0c6bab4be1b2488132c29055504936e8 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:22:42 +0100 Subject: [PATCH 09/16] fix syntax errors --- projects/ngrx.io/content/guide/effects/index.md | 10 +++++----- projects/ngrx.io/content/guide/effects/lifecycle.md | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 1715c918c0..d35d0a9b3e 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -41,7 +41,7 @@ import { CommonModule } from '@angular/common'; <li *ngFor="let movie of movies"> {{ movie.name }} </li> - ` + `, standalone: true, imports: [CommonModule, MoviesService], }) @@ -148,8 +148,8 @@ export class MoviesEffects { .pipe( map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })), catchError(() => EMPTY) - )); - ) + )) + ); }); } @@ -376,8 +376,8 @@ export class CollectionEffects { () => { return this.actions$.pipe( ofType(CollectionApiActions.addBookSuccess), - concatLatestFrom(action => this.store.select(fromBooks.getCollectionBookIds)), - tap(([action, bookCollection]) => { + concatLatestFrom(_action => this.store.select(fromBooks.getCollectionBookIds)), + tap(([_action, bookCollection]) => { if (bookCollection.length === 1) { window.alert('Congrats on adding your first book!'); } else { diff --git a/projects/ngrx.io/content/guide/effects/lifecycle.md b/projects/ngrx.io/content/guide/effects/lifecycle.md index 9b02ca1960..7b7784cf11 100644 --- a/projects/ngrx.io/content/guide/effects/lifecycle.md +++ b/projects/ngrx.io/content/guide/effects/lifecycle.md @@ -109,10 +109,10 @@ import { EffectsModule, EFFECTS_ERROR_HANDLER } from '@ngrx/effects'; import { MoviesEffects } from './effects/movies.effects'; import { CustomErrorHandler, isRetryable } from '../custom-error-handler'; -export function effectResubscriptionHandler>T extends Action<( - observable$: Observable>T<, +export function effectResubscriptionHandler<T extends Action>( + observable$: Observable<T>, errorHandler?: CustomErrorHandler -): Observable>T< { +): Observable<T> { return observable$.pipe( retryWhen(errors => errors.pipe( @@ -142,7 +142,7 @@ bootstrapApplication( useClass: CustomErrorHandler } ], - } + } ) From ee547e979adbfe7a11244abede1b724f0c352d6a Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:27:50 +0100 Subject: [PATCH 10/16] reset formatting --- .../ngrx.io/content/guide/effects/index.md | 61 +++++-------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index d35d0a9b3e..935925772a 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -1,16 +1,10 @@ # @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 @@ -25,10 +19,7 @@ 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. @@ -79,13 +70,9 @@ 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. import { Component, inject } from '@angular/core'; @@ -110,10 +97,7 @@ export class MoviesPageComponent implements OnInit { } -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 @@ -164,12 +148,7 @@ 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 `switchMap`. The example below shows the `loadMovies$` effect handling errors when fetching movies. import { Injectable, inject } from '@angular/core'; @@ -197,17 +176,13 @@ export class MoviesEffects { } -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'; @@ -248,9 +223,7 @@ 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.
@@ -306,8 +279,7 @@ providers: [ ## 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: @@ -354,8 +326,7 @@ export class AuthEffects { 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. @@ -400,11 +371,9 @@ 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, inject } from '@angular/core'; From 7c1a345e66485b882dd519358b03cfa992406fc5 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Tue, 3 Dec 2024 09:22:21 +0100 Subject: [PATCH 11/16] fix: marko review - delete standalone/import from service, add dep imports, typing --- projects/ngrx.io/content/guide/effects/index.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 935925772a..056bb5deb4 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -50,16 +50,18 @@ export class MoviesPageComponent { You also have the corresponding service that handles the fetching of movies. +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + @Injectable({ providedIn: 'root', - standalone: true, - imports: [HttpClient], }) export class MoviesService { private http = inject(HttpClient); - getAll() { - return this.http.get('/movies'); + getAll(): Observable { + return this.http.get('/movies'); } } From dc976be74a3e5847e63ab024ce50d19ac08b0475 Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Tue, 3 Dec 2024 09:25:00 +0100 Subject: [PATCH 12/16] fix: marko review - return of deleted alert with reference to functional effects --- projects/ngrx.io/content/guide/effects/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 056bb5deb4..33ec0ee8a0 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -113,6 +113,12 @@ Effects are injectable service classes with distinct parts: - Effects are subscribed to the `Store` observable. - Services are injected into effects to interact with external APIs and handle streams. +
+ +**Note:** Since NgRx v15.2, classes are not required to create effects. Learn more about functional effects [here](#functional-effects). + +
+ To show how you handle loading movies from the example above, let's look at `MoviesEffects`. From d0c538e14914a9dfe7a4c32a6f2fb80a27ec4a1f Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Tue, 3 Dec 2024 09:35:37 +0100 Subject: [PATCH 13/16] fix: marko review - add inject import --- projects/ngrx.io/content/guide/effects/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 33ec0ee8a0..59381f07c4 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -122,7 +122,7 @@ Effects are injectable service classes with distinct parts: To show how you handle loading movies from the example above, let's look at `MoviesEffects`. -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { EMPTY } from 'rxjs'; import { map, exhaustMap, catchError } from 'rxjs/operators'; From 0b1d48154d7dd3782ff211e4988ee0be544b2f4b Mon Sep 17 00:00:00 2001 From: adrianromanski Date: Tue, 3 Dec 2024 09:38:17 +0100 Subject: [PATCH 14/16] fix: marko review - added back using the standalone api section --- .../ngrx.io/content/guide/effects/index.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 59381f07c4..2645542241 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -270,6 +270,33 @@ Services used in root-level effects are **not** recommended to be used with serv
+### 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. + + +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()`, (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. + +
+ ### Alternative Way of Registering Effects You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`. From adae29c73f85d080d308ff77545ac7e1029c9258 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:48:13 +0100 Subject: [PATCH 15/16] reword Registering Effects --- .../ngrx.io/content/guide/effects/index.md | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index 2645542241..da188e343e 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -237,15 +237,17 @@ It's recommended to inject all dependencies as effect function arguments for eas ## Registering Effects +Effect classes and functional effects are registered using the `provideEffects` method. + +At the root level, effects are registered in the `providers` array of the application configuration. +
-Registering an effects class multiple times (for example in different lazy loaded features) does not cause the effects to run multiple times. +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.
-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'; import { provideStore } from '@ngrx/store'; @@ -263,17 +265,15 @@ bootstrapApplication(AppComponent, { }); +Feature-level effects are registered in the `providers` array of the route config. +The same `provideEffects()` method is used to register effects for a feature. +
-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. +Registering an effects class multiple times (for example in different lazy loaded features) does not cause the effects to run multiple times.
-### 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. - import { Route } from '@angular/router'; import { provideEffects } from '@ngrx/effects'; @@ -291,12 +291,6 @@ 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. - -
- ### Alternative Way of Registering Effects You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`. From 0f9d96256f60c837b7558e9436e1991717aa7079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Stanimirovi=C4=87?= Date: Mon, 9 Dec 2024 22:41:20 +0100 Subject: [PATCH 16/16] Update projects/ngrx.io/content/guide/effects/index.md --- projects/ngrx.io/content/guide/effects/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngrx.io/content/guide/effects/index.md b/projects/ngrx.io/content/guide/effects/index.md index da188e343e..d8b883c96a 100644 --- a/projects/ngrx.io/content/guide/effects/index.md +++ b/projects/ngrx.io/content/guide/effects/index.md @@ -34,7 +34,7 @@ import { CommonModule } from '@angular/common'; </li> `, standalone: true, - imports: [CommonModule, MoviesService], + imports: [CommonModule], }) export class MoviesPageComponent { private moviesService = inject(MoviesService);