What forRoot and forChild have to do with lazy loading
You just created a beautiful Angular component. Now, being the exemplary developer that you are, you want to share it with others.
In Angular, you achieve this by moving it to a separate NgModule. The next step is to make your Angular module customizable. Dependency injection is the key to success!
However, be careful with lazy loading.
Please enjoy this walkthrough and these tips and tricks for configuration in Angular. Let’s make it awesome!
Step 1: the FooModule
Imagine a simple FooModule
, with a FooComponent
:
import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
})
export class FooModule {}
The reason we added the
FooComponent
to the exports
array, is that other modules that import this module, can now use the FooComponent as well.
Now imagine that the FooComponent
shows a label with a prefix:
import { Component, Input } from '@angular/core';
@Component({
selector: 'foo-component',
template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {
@Input() label: string;
prefix = 'Foo';
}
Of course, this is a very simplified example, but you get the idea. This component can now be used as:
<foo-component [label]="'Some label'"></foo-component>
Let’s add some configuration.
Step 2: adding configuration
The second step is to make the prefix
configurable.
Of course we could just add an input property binding with
@Input
, like we did forlabel
. However, imagine that we want to configure the prefix globally for every FooComponent.
A good way to achieve this, is with a configuration class and dependency injection:
import { Component, Input } from '@angular/core';
import { FooConfig } from './foo.config';
@Component({
selector: 'foo',
template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {
@Input() label: string; prefix: string; constructor(config: FooConfig) { this.prefix = config.prefix; } }
This FooConfig
can be a configuration class, e.g.:
export class FooConfig {
prefix = 'Foo';
}
If you want to use a TypeScript interface instead, you have to use an
InjectionToken
. This is because TypeScript interfaces do not exist at runtime.
Let’s provide a default config in our FooModule so that anyone can use this module right away:
import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [FooConfig]
})
export class FooModule {}
Anyone importing this FooModule will use the default config. It is also possible to provide a custom configuration (if preferred):
import { NgModule } from '@angular/core';
import { FooConfig, FooModule } from '@foo/lib';
@NgModule({
imports: [FooModule],
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
Any
FooComponent
in this Angular will now use the custom prefix.
Step 3: use in example application
Imagine an app with the following routing structure:
- AppModule (root module)
- DashboardModule (route “/dashboard”)
- UsersModule (route “/users”)
Now, when both DashboardModule
and UsersModule
need to render FooComponents, they should also both import the FooModule:
@NgModule({imports: [FooModule]}) // (simplified)
export class DashboardModule {}
@NgModule({imports: [FooModule]}) // (simplified)
export class UsersModule {}
However, we still want to have a global configuration, so we keep providing the FooConfig
in the root module:
@NgModule({
imports: [], // (simplified)
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
This works fine, because the injector of AppModule
is the top-most injector for every other module.
Step 4: optimise for lazy-loading
Now, imagine that the UsersModule
of the previous example app is loaded lazily:
const routes: ROUTES = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
// lazy loading:
{ path: 'users', loadChildren: 'src/app/users/users.module#UsersModule' }
];
@NgModule({
imports: [
RouterModule.forRoot(routes),
DashboardModule // contains the dashboard route
]
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
This creates an issue: the UsersModule – which is loaded lazily – will not inherit the custom config from the root module, but the default config from the FooModule.
What happened?
It seems that a module which is lazily loaded will be the top-most injector at bootstrap time.
To solve this, we have to:
- Import FooModule WITH a default config in root module (which we can optionally override)
- Import FooModule WITHOUT a default config in child modules (in order to use its FooComponent)
Instead of making two different modules, Angular provides a ModuleWithProviders
interface for this:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [], // NOTE: no default config here anymore!
})
export class FooModule {}
export const fooModuleWithConfig: ModuleWithProviders = {
ngModule: FooModule,
providers: [FooConfig]
};
export const fooModuleWithoutConfig: ModuleWithProviders = {
ngModule: FooModule
};
Now we can use the
fooModuleWithConfig
in our root module, and fooModuleWithoutConfig
in other modules:
@NgModule({imports: [fooModuleWithConfig]}) // (simplified)
export class AppModule {}
@NgModule({imports: [fooModuleWithoutConfig]}) // (simplified)
export class UsersModule {}
This will solve our issue. While overriding the default config is still possible.
Step 5: stick to conventions
In order to make the intended use more visible, it is recommended to use the static methods forRoot()
and forChild()
:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [], // NOTE: no default config here anymore!
})
export class FooModule {
static forRoot(config: FooConfig): ModuleWithProviders {
return {
ngModule: FooModule,
providers: [{ provide: FooConfig, useValue: config }]
};
}
static forChild(): ModuleWithProviders {
return {
ngModule: FooModule
};
}
}
An added benefit of this is that we can even require a configuration object as argument for the
forRoot()
method (like above), or merge it with a default one.
Final use
In the root module:
@NgModule({
imports: [FooModule.forRoot({ prefix: 'custom' )]
})
export class AppModule {}
In any (lazy-loaded) child module:
@NgModule({
imports: [FooModule.forChild()]
})
export class UsersModule {}
Note: As .forChild()
in this case does not return any providers at all, you can also just use the “plain” FooModule in child modules:
@NgModule({
imports: [FooModule]
})
export class UsersModule {}
Code examples
- Lazy loading problem: StackBlitz
- Lazy loading solution (with
forRoot()
): StackBlitz
Sharing is caring: go for it!
Great Job. This Exactly I was looking for Thanks a Lot.