Migrating Angular to NgRx functional APIs and effects
In this blog post, I want to describe how I migrated NgRx to its functional APIs and effects to use the latest features and benefits of NgRx and Angular with standalone components.
Migrating Angular to NgRx functional APIs and effects
Using provideStore, provideEffects and provideRouterStore on app root level
I just switched a sample application I use to Angular 16, standalone components, signals and all the latest features. In this first blog post, I want to describe how I used the new NgRx APIs to align it with the new Angular standalone components and to migrate Angular and NgRx to functional APIs and effects.
If you are working with Angular standalone components, you can still import from modules with the importProvidersFrom method, which can be imported from @angular/core
.
This makes it possible to import the providers from a module into your app by using standalone components in your app at the same time. Also NgRx provides the StoreModule
and the EffectsModule
to import and register your state with.
If you are using NgRx in your application you have to provide a root state, even if it is an empty object, and root effects with forRoot
, so that other features of your application can add their feature state in a “forFeature” manner.
In terms of NgRx and using the providers from it, this could look like this:
export const appConfig: ApplicationConfig = {
providers: [
// ...
importProvidersFrom(
StoreModule.forRoot({
router: routerReducer,
auth: authReducer,
}),
EffectsModule.forRoot([AuthEffects]),
StoreRouterConnectingModule.forRoot(),
// ... other 'normal' imports from modules
),
],
};
Which is then used to bootstrap your application in your `main.ts` file like this:
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
As we want to move away from using the importProvidersFrom method and turning to more functional APIs, NgRx also provides these APIs since a while now.
If you now want to register effects and also the state on root level, you can use the provideEffects
and the provideStore
functions from NgRx. Also, a provideRouterStore
is available.
Using this, your syntax comes a little nicer and cleaner.
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideEffects(authEffects),
provideStore({
router: routerReducer,
auth: authReducer,
realtime: realtimeReducer,
}),
provideRouterStore(),
importProvidersFrom(
// .. other modules
),
],
};
You can provide functional effects in the provideEffects
method as well as an array of effect classes here. provideStore
still takes the initial app state as parameter, which is an object, and the provideRouterStore
is needed for the router store to work, to connect the router with the NgRx store and make routes, params and queryparams accessible.
Using the APIs on feature level
If you are working with features in Angular, which you should 😉, then you also have to register feature state when the feature is lazy loaded. Without modules, of course.
Again, we used the importProvidersFrom
here in the past
export const DOGGOS_ROUTES: Routes = [
{
path: '',
component: MainDoggoComponent,
providers: [
importProvidersFrom(
// register feature reducer
StoreModule.forFeature(featureName, doggosReducer),
// run feature effects
EffectsModule.forFeature([DoggosEffects])
),
],
},
// ... more routes
];
But we can also use the new APIs from here
import { provideEffects } from '@ngrx/effects';
import { createFeature, provideState } from '@ngrx/store';
import * as doggosEffects from './store/doggos.effects';
export const doggosFeature = createFeature({
name: featureName,
reducer: doggosReducer,
});
export const DOGGOS_ROUTES: Routes = [
{
path: '',
component: MainDoggoComponent,
providers: [provideState(doggosFeature), provideEffects([doggosEffects])],
},
// ... more routes
];
Pay attention to the createFeature
method we are using to create a feature state with NgRx and then provide it to provideState
.
Using functional effects
You might have noticed that the effects were imported with import * as doggoEffects from './store/doggos.effects';
. This is because we can also move the effects to functional effects.
If we take those effects which were class style
@Injectable()
export class DoggosEffects {
rateDoggo$ = createEffect(() =>
this.actions$.pipe(
ofType(DoggosActions.rateDoggo),
concatLatestFrom(() => [
this.store.pipe(select(getSelectedDoggo)),
this.store.pipe(select(getAllDoggos)),
this.store.pipe(select(getNextDoggoIndex)),
]),
concatMap(([{ rating }, selectedDoggo, allDoggos, nextDoggoIndex]) => {
const { id } = selectedDoggo;
return this.doggosService.rate(id, rating).pipe(
concatMap(() => {
const newSelectedDoggo = allDoggos[nextDoggoIndex];
return [DoggosActions.selectDoggo({ id: newSelectedDoggo.id })];
})
);
})
)
);
loadDoggos$ = createEffect(() =>
this.actions$.pipe(
ofType(DoggosActions.loadDoggos),
concatLatestFrom(() => this.store.pipe(select(selectQueryParams))),
concatMap(([action, { doggoId }]) => {
return this.doggosService.getDoggos().pipe(
concatMap((doggos) => {
const currentDoggoId = doggoId || doggos[0]?.id || '-1';
this.notificationService.showSuccess('Doggos Loaded');
return [
DoggosActions.loadDoggosFinished({ doggos }),
DoggosActions.selectDoggo({ id: currentDoggoId }),
];
}),
catchError(() => {
this.notificationService.showError();
return of(DoggosActions.loadDoggosError());
})
);
})
)
);
constructor(
private actions$: Actions,
private store: Store,
private doggosService: DoggosService,
private notificationService: NotificationService
) {}
}
And we migrate them to the new functional approach we can drop the constructor completely and also get rid of the class completely. We only provide functions to the outside hence we have to import them with import * as ... syntax
export const rateDoggo = createEffect(
(
actions$ = inject(Actions),
store = inject(Store),
doggosService = inject(DoggosService)
) => {
return actions$.pipe(
ofType(DoggosActions.rateDoggo),
concatLatestFrom(() => [
store.pipe(select(getSelectedDoggo)),
store.pipe(select(getAllDoggos)),
store.pipe(select(getNextDoggoIndex)),
]),
concatMap(([{ rating }, selectedDoggo, allDoggos, nextDoggoIndex]) => {
const { id } = selectedDoggo;
return doggosService.rate(id, rating).pipe(
concatMap(() => {
const newSelectedDoggo = allDoggos[nextDoggoIndex];
return [DoggosActions.selectDoggo({ id: newSelectedDoggo.id })];
})
);
})
);
},
{ functional: true }
);
export const loadDoggos = createEffect(
(
actions$ = inject(Actions),
store = inject(Store),
doggosService = inject(DoggosService),
notificationService = inject(NotificationService)
) => {
return actions$.pipe(
ofType(DoggosActions.loadDoggos),
concatLatestFrom(() => store.pipe(select(selectQueryParams))),
concatMap(([action, { doggoId }]) => {
return doggosService.getDoggos().pipe(
concatMap((doggos) => {
const currentDoggoId = doggoId || doggos[0]?.id || '-1';
notificationService.showSuccess('Doggos Loaded');
return [
DoggosActions.loadDoggosFinished({ doggos }),
DoggosActions.selectDoggo({ id: currentDoggoId }),
];
}),
catchError(() => {
notificationService.showError();
return of(DoggosActions.loadDoggosError());
})
);
})
);
},
{ functional: true }
);
You can see that we pass the used services, actions and the store as parameters in the function which we pass to the createEffect
method. The rest of the effect can stay the same!
In this blog post, we have seen how we can apply and use the new NgRx APIs and functional approach to get our application to the latest stand.
You can find the complete repo here.
https://github.com/FabianGosebrink/doggo-rate-app
Photo by Jerry Zhang on Unsplash