How to reduce duplicated code by using dependency injection in Angular
Using dependency injection to replace redundant logic code
In my current project, there’s a block of code that is used in many places. The code looks like this
@Component({
...
})
export class SomeComponent implements OnInit {
constructor(
private route: ActivatedRoute
) {} ngOnInit(): void {
this.route.params.pipe(
takeUntil(this.destroy$),
map(params => params.id),
filter(v => !!v),
// do something with this id
);
}
}
The above code gets a stream of id from the activated route. In other places, it would be getting the passengerId from activated route, or getting whatever id and do something with it.
So the pattern is that using the ActivatedRoute
service to get something from the route parameters.
In fact, we can reduce this duplicated logic by using dependency injection. Here’s 3 steps to do that.
Declare injection token and factory function
First, we create a file named route-param.token.ts
in a folder, for example core/tokens
export const ROUTE_PARAM_TOKEN = new InjectionToken<Observable<string>>(
'Stream of route param from activated route'
);// if you want to get :id from route, declare this token
export const PARAM_KEY_ID = new InjectionToken<string>(
'static string for :id param key',
// this is the second argument of InjectionToken constructor
// to produce the value, like when you use useFactory
{
factory: () => {
return 'id';
}
}
);// if you want to get :someId from the route, the token should look like this
export const PARAM_KEY_SOME_ID = new InjectionToken<string>(
'static string for :someId param key', {
factory: () => {
return 'someId';
}
}
);export function routeParamFactory(
route: ActivatedRoute,
paramKey: string
): Observable<string> {
// should use paramMap because route.params will be deprecated soon
return route.paramMap.pipe(map((param) => param.get(paramKey)))
}
Declare the token in the provider list of your component
Next is to declare the token you want to use in the providers
list in your component.
@Component({
providers: [
{
provide: ROUTE_PARAM_TOKEN,
useFactory: routeParamFactory,
deps: [ActivatedRoute, PARAM_KEY_ID]
// if you want to get someId, the deps will be
// deps: [ActivatedRoute, PARAM_KEY_SOME_ID]
}
]
})
export class SomeComponent {
constructor() {}
}
Inject the token in constructor and use it in your component
@Component({
providers: [
{
provide: ROUTE_PARAM_TOKEN,
useFactory: routeParamFactory,
deps: [ActivatedRoute, PARAM_KEY_ID]
}
]
})
export class SomeComponent implements Oninit {
constructor(
@Inject(ROUTE_PARAM_TOKEN)
private readonly id$: Observable<string> // should use readonly as much as possible
) {} ngOnInit(): void {
this.id$.pipe(
// do whatever with this id
);
}
}
There are many benefits when you use this approach
- It makes your code cleaner and easy to understand and maintain
- Easier to test you component by mocking the dependency injection token
- It helps reduce duplicated logic in your code
Thanks for reading.