We can transfer any data through our apps, transform and replace it at any level.

So we can make the architecture simpler and more flexible with clear data flow and low coupling. It also makes testing and replacing our dependencies easy.

Nevertheless, I think that DI in Angular applications is used rather modestly. It is usually an injection of service or providing some global data top-down through the app.

In this article, I want to show an alternative way to work with data from DI. The purpose is to simplify components, directives, and services that use it.

Typical DI pattern in Angular

I review Angular code every day during my work and opensource projects. The uses of DI in most apps is limited by the following cases:

  1. Get some Angular entities like ChangeDetectorRef, ElementRef and other from DI.
  2. Get a service to use it in a component.
  3. Get a global config via a token that is declared in the root of the app. For example, make an injection token API_URL in the app.module and get this URL in any entity in the app when you need it.

Sometimes developers transform already existing global token in a more convenient format. A good sample of such a transformation is a WINDOW token from a package @ng-web-apis/common.

Angular has a DOCUMENT token to get a page object in any place of your app. That way your components do not depend on global objects. It is easy to test them and nothing gets broken by Server-Side Rendering.

If you want to get a window object more often, you can make the following token:

import {DOCUMENT} from '@angular/common';
import {inject, InjectionToken} from '@angular/core';

export const WINDOW = new InjectionToken<Window>(
    'An abstraction over global window object',
    {
        factory: () => {
            const {defaultView} = inject(DOCUMENT);

            if (!defaultView) {
                throw new Error('Window is not available');
            }

            return defaultView;
        },
    },
);

When someone requests WINDOW token the first time from DI, Angular executes a token factory. It takes a DOCUMENT object and gets a link to a window object from it.

If you feel uncertain in your DI skills, try the first free chapter in angular.institute. There is much information about how DI works and how to use it effectively

Now I want to propose another way to make such transformations to move them into providers field of component or directive that injects the result.

Private providers

In my team, we use DI often and notice that sometimes we need to transform data coming from DI before using it. In other words, our component depends on one type of data, but we inject another one and do some transformations inside it.

Let’s consider an influential example. Erin Coughlan gave a talk 'The Architecture of Components' in the Angular Connect conference. You can watch it here.

If it is not convenient for you to watch a video now, I describe the case here.

So we have:

  • A component that shows information about some entity called “organization”
  • Query-param of the route with the ID of the organization to work with
  • A service that gets ID and returns an Observable with information about an organization

What we want to do:

We want to take ID from query-params, call a service method with it, and get a stream with organization information in return. This information is showed in a component.

Let’s look at the three solutions:

1. How you should not do it

Sometimes I see the following work with data in a component. Please, do not do this:

@Component({
    selector: 'organization',
    templateUrl: 'organization.template.html',
    styleUrls: ['organization.style.less'],
    changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class OrganizationComponent implements OnInit {
    organization: Organization;
 
    constructor(
        private readonly activatedRoute: ActivatedRoute,
        private readonly organizationService: OrganizationService,
    ) {}
 
    ngOnInit() {
        this.activatedRoute.params
            .pipe(
                switchMap(params => {
                    const id = params.get('orgId');
 
                    return this.organizationService.getOrganizationById$(id);
                }),
            )
            .subscribe(organization => {
                this.organization = organization;
            });
    }
 }

And it is used in a template like that:

<p *ngIf="organization">
    {{organization.name}} from {{organization.city}}
</p>

This code works but it has some problems:

  • The 'organization' field is not defined at component creation. That is why there is a time when we can get 'undefined'. If we have non-strict TypeScript, we break typings. Or we can write the right type: organization?: Organization and now we need to add several checks.
  • This code is harder to support. For example, next time we need one more param and we add one more subscription into ngOnInit. It will harder to read and understand each time because of implicit variables and unclear data flow.
  • We can meet some problems with change detection and updating our component using OnPush strategy.

2. Let’s do it well

Erin made it well in her talk. Her sample from presentation looks like this:

@Component({
    selector: 'organization',
    templateUrl: 'organization.template.html',
    styleUrls: ['organization.style.less'],
    changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class OrganizationComponent {
    readonly organization$: Observable<Organization> = this.activatedRoute.params.pipe(
        switchMap(params => {
            const id = params.get('orgId');
            return this.organizationService.getOrganizationById$(id);
        }),
    );
  
    constructor(
        private readonly activatedRoute: ActivatedRoute,
        private readonly organizationService: OrganizationService,
    ) {}
 }

And it is used in a template this way:

<p *ngIf="organization$ | async as organization">
   {{organization.name}} from {{organization.city}}
</p>

This code works well and has no disadvantages of the previous approach: it looks rather neat and we have no unnecessary fields. If we want to extend the component with a similar stream, we just add another one — it is easy to do. We do not need to touch the code of the previous one to add a new stream.

In addition, the data stream is cleaner: we only have a stream that is created in a moment the component class is created. When it emits data, the information in our template will be shown.

3. Let’s try to do it cooler with private providers

Let’s take a closer look at the previous solution.

In fact, the component does not depend on the router and even on OrganizationService. It depends on organization$. But there is no such thing in our dependency injection tree, so we have to transform data in the component.

But what if you transform the data before it enters the component? Let’s write the Provider for the component in which we place all transformations.

For convenience, we can put the providers into a separate file next to the component and get the following file structure:

So we have organization.providers.ts file with Provider that transforms data and an injection token to get it in the component:

// token to access a stream with the information you need
export const ORGANIZATION_INFO = new InjectionToken<Observable<Organization>>(
    'A stream with current organization information'
);

export const ORGANIZATION_PROVIDERS: Provider[] = [
    {
        provide: ORGANIZATION_INFO,
        deps: [ActivatedRoute, OrganizationService],
        useFactory: organizationFactory,
    },
];

export function organizationFactory(
    { params }: ActivatedRoute,
    organizationService: OrganizationService
): Observable<Organization> {
    return params.pipe(
        switchMap((params) => {
            const id = params.get('orgId');

            return organizationService.getOrganizationById$(id);
        })
    );
}

Let’s define an array of providers for the component. Token ORGANIZATION_INFO gets a value from a factory that transforms data.

A note about DI: deps allow us to get some entities from DI tree and send them as arguments into a token factory. This way you can get any entity from DI. You can even use DI decorators:
{
    provide: ACTIVE_TAB,
    deps: [
        [new Optional(), new Self(), RouterLinkActive],
    ],
    useFactory: activeTabFactory,
}

So we need to set our providers for the component:

@Component({    
    ..    
    providers: [ORGANIZATION_PROVIDERS], 
})

And now we are ready to get it in the component:

@Component({
    selector: 'organization',
    templateUrl: 'organization.template.html',
    styleUrls: ['organization.style.less'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [ORGANIZATION_PROVIDERS],
 })
 export class OrganizationComponent {
    constructor(
        @Inject(ORGANIZATION_INFO) readonly organization$: Observable<Organization>,
    ) {}
 }

The whole class is just a single line of code with data injecting.

The template is not changed:

<p *ngIf="organization$ | async as organization">
    {{organization.name}} from {{organization.city}}
</p>

What does this approach give us?

  1. Clear dependencies: a component does not inject any data that it does not need. It works only with entities that it needs to show data in a template.
  2. It is testable: we can easily test a provider because its factory is just a function. It’s also easier for us to test a component: in the tests, we don’t have to build a dependency tree and replace many entities  —  we just pass ORGANIZATION_INFO token with stub data.
  3. It scales: do you want your component to work with another type of data? No problem. We just change one line of code. If you need to edit a transformation logic, change a factory. If you need some new data, add another token because you can have an unlimited amount of tokens in your providers array.

After we started to use this approach, many of our components and directive look cleaner and simpler. Separating the logic of data transformation and data showing makes it easy to modify logic or expand functionality. It is also easier to catch bugs because you can define a problem area: the problem can be in data transformations or in its showing to a user.

We at Jamigo.app really max out this approach. If you want to read more about DI-based architecture, show your interest by liking this Tweet so my friend Alex would write an article about it :)

In conclusion

The described approach cannot fix all your design issues. You shouldn’t add providers to any small case: sometimes the code is clearer if you transform data in a class method or use Angular pipes.

Nevertheless, I hope that private providers can help you simplify your components with a lot of dependencies or give you an alternative when gradually refactoring large pieces of logic.