Two methods of declaring modules in Angular

Picture of a modular city.
Photo by veeterzy on Unsplash

Recently I came upon this interesting code in an Angular project where static methods were used to return modules, just like this:

@NgModule({
  imports: [ ... ],
  declarations: [ ... ],
  providers: [FunService]
})
export class FunModule {

  static forRoot(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [ AwesomeService ],
   };
  }
}

I asked myself a question:

Does the module returned by the static forRoot method come with the providers declared in @NgModule() decorator?

I thought that is an interesting question because Angular can act either way. The method forRoot returns an object that implements ModuleWithProviders which consists of a reference to an Angular ngModule and a list of providers. In this exact case forRoot returns object that has a reference to the FunModule and an array with one service AwesomeService. Taking that into consideration Angular may return providers declared in NgModule() or from providers declared in the static forRoot method. The alternative is that it can merge them.

In this article, I would like to answer the previously asked question but first let's start with the basic theory.

The theory of Angular modules.
The theory of Angular modules.

Basics

Angular comes with two methods of defining NgModules. You can either do it with a @NgModule() decorator or use the interface ModulesWithProviders<T> and return it from a function. There is also another method where you use the mix of these two mechanisms.

Let's take a look at the example below.

@NgModule({
  imports: [CommonModule],
  providers: [FunService],
  declarations: [FunComponent]
})
export class FunModule {

  static forFun(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [ AwesomeService ]
    };
  }
}

You can see that you have two types of module declarations. The most common one using @NgModule({}):

@NgModule({
  imports: [CommonModule],
  providers: [FunService],
  declarations: [FunComponent]
})
export class FunModule {}

And the second one uses a method that returns an object implementing the interface ModuleWithProviders<T>:

static forRoot(): ModuleWithProviders<FunModule> {
  return {
    ngModule: FunModule,
    providers: [ AwesomeService ]
  };
}

There is an unwritten rule in the Angular world that the forRoot needs to be a static method declared in the same module, but there are no technical requirements that force us to use it this way, we can also write it as a regular function:

export function forRoot(): ModuleWithProviders<FunModule> {
  return {
    ngModule: FunModule,
    providers: [ AwesomeService ]
  };
}

We can see that forRoot returns an object which consists of providers and a reference to the FunModule, so the module declared with @NgModule(). We can assume that everything that is defined inside of the NgModule is also defined in a dynamic module returned by the forRoot.

What about providers that are something additional to the FunModule? I guess you can assume that providers can be treated differently.

That's not enough.
That's not enough.

ForRoot providers

Let's have a look at this simple example:

@NgModule({
  imports: [ ... ],
  providers: [FunService]
})
export class FunModule {

  static forRoot(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [ AwesomeService ],
    };
  }
}

The question is:

Will FunModule.forRoot() return a module that also includes providers from the FunModule?

To be more precise, will the dynamically created module also return the FunService:

{
  ngModule: FunModule,
  providers: [
    AwesomeService
  ]
};

To figure it out I created this code example on the stackblitz. You can check it out here.

I have added the FunModule.forRoot to the AppModule, then I introduced the FunService in an AppComponent to see if it will be correctly injected.

AppComponent with the FunService injected.
AppComponent with the FunService injected.

And as you can see in the picture it works correctly.

FunService is correctly injected.
FunService is correctly injected.

That means that Angular merges together providers from NgModule and declarations from the static forRoot method. So there is our answer to the previously asked question. The ModuleWithProviders returns combined an array of providers.

Good job - work is done.
Good job - work is done.

Overwrite providers

Knowing all of that let's think about how can we leverage that mechanism to our advantage 🤔. An interesting example of this behavior is that we can provide different implementations of a selected service.

Let's say in our application we have a service that makes an HTTP call to the backend to get data. We can create a different implementation: one that uses GraphQL instead of HTTP and another which returns a defined set of data that can be used for testing.

There is a couple of reasonable use cases that come to mind:

  • different methods of acquiring data,
  • testing new UX design,
  • presenting example components in Storybook,
  • unit testing purposes.
export class FunModule {

  static forTesting(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [
        // Mock services gives mock data instead of real one
        { provide: FunService useClass: MockFunService }
      ]
    };
  }

  static forHttp(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [
        // Provides implementation of Http
        // can be change to the Axios or GraphQL
        { provide: FunService useClass: HttpFunService }
      ]
    };
  }

  static forStorybook(): ModuleWithProviders<FunModule> {
    return {
      ngModule: FunModule,
      providers: [
        // Specific implementation required by the Storybook
        { provide: FunService useClass: StorybookFunService }
      ]
    };
  }

}

Each of the static methods returns an implementation of the FunService exactly tailored for the specific use case. This example of designing your code creates an architecture where closely coupled code is located together. Programmers can easily analyze how code works and find all code related to the domain.

Code related to the same domain should be close together.
Code related to the same domain should be close together.

Summary

Angular gives us lots of tools so we can write our application in the best way possible.NgModulesare definitely state-of-the-art mechanisms that allow us to scale our applications in a better way. With the use of theModuleWithProvidersand declaring providers in static methods, we can create modules that come with the solutions for different use cases. This feature comes in handy when we want to write our application in a modular way, where every module provides a well thought wholesome solution.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.