DEV Community

Cover image for Using NgRx Packages with Standalone Angular Features
Marko Stanimirović for NgRx

Posted on

Using NgRx Packages with Standalone Angular Features

In this article, we'll look into the standalone Angular APIs introduced in version 14. We will then explore ways how to use NgRx packages with standalone features.

Contents


Standalone Angular APIs

With standalone Angular APIs, we can build Angular applications without NgModules. In other words, components, directives, and pipes can be used without declaration in any Angular module.

💡 In Angular 14, standalone APIs are in developer preview and may change in the future without backward compatibility.

Creating Standalone Components

To create a standalone component, we need to set the standalone flag to true and register template dependencies using the imports property within the component configuration. The imports array can accept Angular modules or other standalone components, directives, or pipes:

// header.component.ts

@Component({
  selector: 'app-header',
  template: `
    <a routerLink="/">Home</a>
    <a *ngIf="isAuthenticated$ | async" routerLink="/">Musicians</a>
  `,
  standalone: true,
  // importing modules whose declarables are used in the template
  imports: [CommonModule, RouterModule],
})
export class HeaderComponent {
  readonly isAuthenticated$ = this.authService.isAuthenticated$;

  constructor(private readonly authService: AuthService) {}
}

// app.component.ts

@Component({
  selector: 'app-root',
  template: `
    <app-header></app-header>
    <router-outlet></router-outlet>
  `,
  standalone: true,
  // importing `HeaderComponent` as a template dependency
  imports: [RouterModule, HeaderComponent],
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

AppModule is no longer required to bootstrap the application. Instead, we can use the bootstrapApplication function from the @angular/platform-browser package that accepts the root component as an input argument:

// main.ts

bootstrapApplication(AppComponent);
Enter fullscreen mode Exit fullscreen mode

The bootstrapApplication function accepts an object with providers as a second argument, so we can provide services at the root level as follows:

bootstrapApplication(AppComponent, {
  providers: [
    { provide: ErrorHandler, useClass: CustomErrorHandler },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Interop with Angular Modules

Now the question is, how to provide services from existing Angular modules. Fortunately, there is a new function importProvidersFrom from the @angular/core package that accepts a sequence of Angular modules as an input argument and returns their providers as a result:

const providers = importProvidersFrom(
  HttpClientModule,
  // ... other modules
);
Enter fullscreen mode Exit fullscreen mode

Providers returned by the importProvidersFrom function can be registered at the root level in the following way:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(HttpClientModule),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Configuring Angular Router

In Angular 14, there is an option to register providers at the route level by adding the providers array to the Route object. This gives the ability to define feature-level providers in the following way:

// musicians.routes.ts

export const musiciansRoutes: Route[] = [
  {
    path: '',
    // registering providers for the route and all its children
    providers: [
      { provide: MusiciansService, useClass: MusiciansHttpService },
      importProvidersFrom(NgModule1, NgModule2),
    ],
    children: [
      {
        path: '',
        component: MusicianListComponent,
      },
      {
        path: ':id',
        component: MusicianDetailsComponent,
        canActivate: [MusicianExistsGuard],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

Then, we can lazy load feature routes using the loadChildren property in the application routes configuration:

// app.routes.ts

export const appRoutes: Route[] = [
  { path: '', component: HomeComponent },
  {
    path: 'musicians',
    // importing `musiciansRoutes` using the `loadChildren` property
    loadChildren: () =>
      import('@musicians/musicians.routes').then(
        (m) => m.musiciansRoutes
      ),
  },
];
Enter fullscreen mode Exit fullscreen mode

The next step is to register application routes using the RouterModule as follows:

// main.ts

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(RouterModule.forRoot(appRoutes)),
  ],
});
Enter fullscreen mode Exit fullscreen mode

When bootstrapping the application, Angular will initialize the root RouterModule, register application routes, and provide Router, ActivatedRoute, and other providers from the RouterModule at the root level.


Angular Modules from NgRx Packages

As we have seen in the case of the RouterModule, Angular modules are not only used to declare components or provide services. They are also used to configure various application and library functionalities. In the case of NgRx, we use the EffectsModule.forRoot method to provide the Actions observable at the root level of an Angular application, initialize the effects runner, and run root effects. Therefore, importing root modules from other NgRx packages will configure their functionalities and/or provide services:

// app.module.ts

@NgModule({
  imports: [
    // provide `Store` at the root level
    // register initial reducers
    // initialize runtime checks mechanism
    StoreModule.forRoot({ router: routerReducer, auth: authReducer }),
    // connect NgRx Store with Angular Router
    StoreRouterConnectingModule.forRoot(),
    // connect NgRx Store with Redux Devtools extension
    StoreDevtoolsModule.instrument(),
    // provide `Actions` at the root level
    // initialize effects runner
    // run root effects
    EffectsModule.forRoot([RouterEffects, AuthEffects]),
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Also, NgRx exposes APIs for registering additional reducers and effects in feature modules:

// musicians.module.ts

@NgModule({
  imports: [
    // register feature reducer
    StoreModule.forFeature('musicians', musiciansReducer),
    // run feature effects
    EffectsModule.forFeature([MusiciansApiEffects]),
  ],
})
export class MusiciansModule {}
Enter fullscreen mode Exit fullscreen mode

Using NgRx Modules with Standalone Angular APIs

Similar to the root RouterModule, NgRx modules can be configured at the application level using the bootstrapApplication function:

// main.ts

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      RouterModule.forRoot(appRoutes),

      // configure NgRx modules
      StoreModule.forRoot({
        router: routerReducer,
        auth: authReducer,
      }),
      StoreRouterConnectingModule.forRoot(),
      StoreDevtoolsModule.instrument(),
      EffectsModule.forRoot([RouterEffects, AuthEffects])
    ),
  ],
});
Enter fullscreen mode Exit fullscreen mode

The feature reducer and effects can be lazily registered in the route configuration for a specific feature as follows:

// musicians.routes.ts

export const musiciansRoutes: Route[] = [
  {
    path: '',
    providers: [
      importProvidersFrom(
        // register feature reducer
        StoreModule.forFeature('musicians', musiciansReducer),
        // run feature effects
        EffectsModule.forFeature([MusiciansApiEffects])
      ),
    ],
    children: [
      {
        path: '',
        component: MusicianListComponent,
      },
      {
        path: ':id',
        component: MusicianDetailsComponent,
        canActivate: [MusicianExistsGuard],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

Standalone NgRx APIs

Instead of using NgModules to configure NgRx packages and/or provide their services, we could use functions for a "module-free" developer experience. For example, we could use a function named provideStore instead of StoreModule.forRoot. The same principle can be applied to other NgRx packages. Using standalone NgRx functions would look like this:

// main.ts

bootstrapApplication(AppComponent, {
  providers: [
    // alternative to `StoreModule.forRoot`
    provideStore({ router: routerReducer, auth: AuthReducer }),
    // alternative to `StoreRouterConnectingModule.forRoot`
    provideRouterStore(),
    // alternative to `StoreDevtoolsModule.instrument`
    provideStoreDevtools(),
    // alternative to `EffectsModule.forRoot`
    provideEffects([RouterEffects, AuthEffects]),
  ),
});
Enter fullscreen mode Exit fullscreen mode

Feature reducers and effects would also be registered using functions instead of NgModules:

// musicians.routes.ts

export const musiciansRoutes: Route[] = [
  {
    path: '',
    providers: [
      // alternative to `StoreModule.forFeature`
      provideStoreFeature('musicians', musiciansReducer),
      // alternative to `EffectsModule.forFeature`
      provideFeatureEffects([MusiciansApiEffects]),
    ],
    children: [
      {
        path: '',
        component: MusicianListComponent,
      },
      {
        path: ':id',
        component: MusicianDetailsComponent,
        canActivate: [MusicianExistsGuard],
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

💡 The design of standalone NgRx APIs is still under consideration. If you have any suggestions, leave a comment here.

Source Code

The source code of the proposed standalone NgRx APIs and sample project is available here.

Resources

Peer Reviewers

Many thanks to Tim Deschryver and Brandon Roberts for reviewing this article!

Discussion (1)

Collapse
tleperou profile image
Thomas Lepérou

Thank you ! Quiet exciting to see substantial things happening in Angular's eco system