Side-effects! They are one of the most common tasks in our applications. In Angular, but build application if we don't take care the component ends with a lot of responsability, like get, process and render the data. But in Angular most of time when we need to get data from an API, instead of put the logic to handle everything related to HTTP requests, we create services to put the logic there, but our components still need to use these services to subscribe to them.
When we use Ngrx, the main idea is for components to trigger actions. These actions then cause the reducer to make the necessary changes in the state and get the updated data using the selectors in the component.
But how I can handle side-effect changes? For example start a http request, get the data and trigger the action with the result? who is responsible to get the data, process and update the state?
let's show a scenario, I need to show a list of players from my state, and the players come from an API. We have two actions to start this process: Players Load
and Player Load Success
.
export const HomePageActions = createActionGroup({
source: 'Home Page',
events: {
'Accept Terms': emptyProps(),
'Reject Terms': emptyProps(),
'Players Load': emptyProps(),
'Player Loaded Success': props<{ players: Array<any> }>(),
},
});
We have to have a separation, so we create the players.service.ts
with the responsibility to get the data.
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay, map } from 'rxjs';
import { environment } from '../../environments/environment';
import { Player } from '../entities/player';
@Injectable({ providedIn: 'root' })
export class PlayersService {
private _http = inject(HttpClient);
public getPlayers() {
return this._http
.get<{ data: Array<Player> }>(`${environment.apiUrl}/players`, {
headers: {
Authorization: `${environment.token}`,
},
})
.pipe(
map((response) => response.data),
delay(5000),
);
}
}
But, I can't change the state
in the reducer because it is a function, I can't do async task or dispatch actions from there, the only place available is to use the same component to dispatch the actions when get the data.
Open the home.component.ts
, we inject the PlayersService
, in the onOnInit
lifecycle dispatch the HomePageActions.playersLoad()
action to set the loading to true, and subscribe to the this._playersService.getPlayers()
after get the data then dispatch the playerLoadedSuccess
action with the response.
The code looks like:
export class HomeComponent implements OnInit {
private _store = inject(Store);
private _playersService = inject(PlayersService);
public $loading = this._store.selectSignal(selectLoading);
public $players = this._store.selectSignal(selectPlayers);
/**
others properties removed to d to keep simplicity.
**/
public ngOnInit(): void {
this._store.dispatch(HomePageActions.playersLoad());
this._playersService.getPlayers().subscribe(players => {
this._store.dispatch(HomePageActions.playerLoadedSuccess({
players
}))
})
}
The previous code works, but why does the home.component
have to subscribe
to the service and also dispatch the action when the data arrives? Why does the home.component
need to know who is responsible for loading the data? The home component only needs to trigger actions and react to state changes.
This is where NgRx Effects are useful. They take actions, perform the necessary tasks, and dispatch other actions.
The Effects
What is an effect? It is a class like a service with the @Injectable
decorator and the Actions
injected. The Actions
service help to listen each action dispatched after the reducer.
@Injectable()
export class HomeEffects {
private _actions = inject(Actions);
}
We declare a field using the createEffe
ct
function, any action returned from the effect stream is then dispatch back to the Store
and the Actions are filtered using a ofType operator to takes one or more actions. The of action is then flatten and mapped into a new observable using any high-orders operator like concatMap
, exhaustMap
, switchMap
or mergeMap
.
loadPlayers = createEffect(() =>
this._actions.pipe(
ofType(HomePageActions.playersLoad)
));
Since the version 15.2 we also have functional effects, instead to use a class use the same createEffect
function to create the effects.
export const loadPlayersEffect = createEffect(
(actions$ = inject(Actions)) => {
});
But how does it work? Well, the component triggers the load product action, then the effect listens for this action. Next, we inject the service to get the data and trigger an action with the data. The reducer then listens for this action and makes the change.
Does it seem like too many steps? Let me show you how to refactor our code to use Effects!
Moving To Effects
It's time to start using effects
in our project. We continue with the initial project of NgRx, clone it, and switch to the action-creators
branch.
git clone https://github.com/danywalls/start-with-ngrx.git
git switch feature/using-selectors
Next, install effects @ngrx/effects
package from the terminal.
npm i @ngrx/effects
Next, open the project with your favorite editor and create a new file src/app/pages/about/state/home.effects.ts
. Declare a loadPlayersEffect
using the createEffect
function, inject Actions
and PlayersService
, and then pipe the actions.
import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PlayersService } from '../../../services/players.service';
export const loadPlayersEffect = createEffect(
(actions$ = inject(Actions), playersService = inject(PlayersService)) => {
return actions$.pipe()
)
);
Use ofType
to pipe the actions and filter by the HomePageActions.playersLoad
action type.
loadPlayers = createEffect(() =>
this._actions.pipe(
ofType(HomePageActions.playersLoad)
)
);
Use the concatMap
operator to get the stream from the action, use the playerService and call the getPlayers()
method and use map
to dispatch HomePageActions.playerLoadedSuccess({ players })
.
concatMap(() =>
this._playersService
.getPlayers()
.pipe(
map((players) => HomePageActions.playerLoadedSuccess({ players })),
),
),
After the map, handle errors using the catchError
operator. Use the of
function to transform the error into a HomePageActions.playerLoadFailure
action and dispatch the error message.
catchError((error: { message: string }) =>
of(HomePageActions.playerLoadFailure({ message: error.message })),
),
The final code looks like:
import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PlayersService } from '../../../services/players.service';
import { HomePageActions } from './home.actions';
import { catchError, concatMap, map, of } from 'rxjs';
export const loadPlayersEffect = createEffect(
(actions$ = inject(Actions), playersService = inject(PlayersService)) => {
return actions$.pipe(
ofType(HomePageActions.playersLoad),
concatMap(() =>
playersService.getPlayers().pipe(
map((players) => HomePageActions.playerLoadedSuccess({ players })),
catchError((error: { message: string }) =>
of(HomePageActions.playerLoadFailure({ message: error.message })),
),
),
),
);
},
{ functional: true },
);
We have the effect ready, its time to register it in the app.config, so import the home.effect and use the provideEffects function pass the homeEffect
The app.config looks like:
import * as homeEffects from './pages/home/state/home.effects';
export const appConfig = {
providers: [
provideRouter(routes),
provideStore({
home: homeReducer,
}),
provideStoreDevtools({
name: 'nba-app',
maxAge: 30,
trace: true,
connectInZone: true,
}),
provideEffects(homeEffects), //provide the effects
provideAnimationsAsync(),
provideHttpClient(withInterceptors([authorizationInterceptor])),
],
};
We have registered the effect, so it's time to refactor the code in the HomeComponent
. Remove the injection of the players service, as we no longer need to subscribe to the service.
The home component looks like:
export class HomeComponent implements OnInit {
private _store = inject(Store);
public $loading = this._store.selectSignal(selectLoading);
public $players = this._store.selectSignal(selectPlayers);
public $acceptTerms = this._store.selectSignal(selectAcceptTerms);
public $allTasksDone = this._store.selectSignal(selectAllTaskDone);
public ngOnInit(): void {
this._store.dispatch(HomePageActions.playersLoad());
}
onChange() {
this._store.dispatch(HomePageActions.acceptTerms());
}
onRejectTerms() {
this._store.dispatch(HomePageActions.rejectTerms());
}
}
Done! Our app is now using effects, and our components are clean and organized!
Recap
We learned how to handle side-effects like HTTP requests and clean up components that have too many responsibilities. By using actions, reducers, and effects to manage state and side-effects. We can refactor our component to use NgRx Effects for fetching data from an API. By moving the data-fetching logic to effects, components only need to dispatch actions and react to state changes, resulting in cleaner and more maintainable code.
Top comments (0)