The standard approach to Angular DI
In my daily work and while reviewing open-source projects, I see Angular code all the time. The way DI gets used in most applications usually falls into one of these categories:
- Pulling Angular primitives like
ChangeDetectorRef,ElementRef, and similar objects straight from DI. - Grabbing a service that a component relies on.
- Reading a global configuration through a token defined at the app root. A typical example is declaring an
API_URLinjection token insideapp.moduleand then fetching that URL wherever it’s needed.
There are also cases where developers reshape an existing global token into something more ergonomic. A good illustration is the WINDOW token shipped in the @ng-web-apis/common package.
Angular gives us the DOCUMENT token so we can access the page object without relying on globals. This keeps components decoupled from the environment and makes them safe for Server-Side Rendering and easier to test.
If you find yourself reaching for the _window_ object frequently, you might define a token like this:
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;
},
},
);
The very first time something requests the WINDOW token, Angular runs its factory, which grabs the DOCUMENT and pulls the window reference out of it.
If DI still feels shaky, the first chapter of the angular.institute course is free. It goes deep into how DI operates and how you can use it well.
What I’d like to suggest is a different angle: moving these kinds of transformations into the providers array of the component or directive that ultimately consumes the result.
Introducing private providers
In our team, DI is something we rely on heavily. Over time we noticed that the data coming from DI often needs tweaking before it can be used. In practical terms, the component wants one shape of data, but we end up injecting something else and doing conversions inside the component body.
Let’s walk through a memorable example. Erin Coughlan presented “The Architecture of Components” at Angular Connect. You can watch the talk here.
If you’d rather skip the video, here’s the gist.
The situation looks like this:
- A component displays details about an entity referred to as “organization”.
- A route query-param carries the ID of the organization in question.
- A service takes that ID and returns an Observable that resolves to the organization’s info.
The goal:
Pull the ID from query-params, feed it into the service, and get back a stream of organization data. The component then renders that data.
Let’s compare three ways to get there:
1. A pattern to avoid
I keep running into components that handle data like this. Please steer clear of it:
@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;
});
}
}
Here’s how it shows up in the template:
<p *ngIf="organization">
{{organization.name}} from {{organization.city}}
</p>
This works on the surface, but it creates real friction:
- At the moment the component is instantiated, the ‘organization’ property doesn’t exist yet. That opens the door to ‘undefined’ values. With non-strict TypeScript you weaken your typing; with strict typing you end up declaring
organization?: Organizationand sprinkling null-checks everywhere. - Maintenance becomes tedious. The moment you need another parameter, you add yet another subscription in
ngOnInit. As this accumulates, the component gets harder to read and data flow becomes murky. - When using OnPush change detection, keeping the view in sync can get tricky.
2. A solid solution
Erin showed a much better way during her talk. Her example looked 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,
) {}
}
The template then consumes it this way:
<p *ngIf="organization$ | async as organization">
{{organization.name}} from {{organization.city}}
</p>
This approach avoids the problems from the first version. The component stays tidy and free of stray fields. Want to add another similar stream? Just drop in another one — no need to touch the existing logic.
On top of that, the data flow is more predictable: the stream gets created at the same moment the component class is born. Whenever it pushes a value, the template updates accordingly.
3. A leaner take using private providers
Now let’s look closer at the previous solution.
Technically, the component doesn’t truly depend on the router or even on OrganizationService. What it actually depends on is organization$. But that stream doesn’t exist anywhere in the DI tree, so the component ends up doing the conversion by hand.
What if we reshape the data before it ever reaches the component? Let’s put those transformations into a Provider defined for that component.
To keep things organized, we can keep that provider in its own file beside the component. The file layout can look like this:

So the _organization.providers.ts_ file holds the Provider, which handles the data transformation, along with an injection token the component uses to fetch the result:
// 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);
})
);
}
The component gets an array of providers. The _ORGANIZATION_INF_O token pulls its value from a factory that performs the transformation.
DI detail: using deps lets you request entities from the DI graph and hand them into the token factory as arguments. This opens the door to any dependency you want — you can even use DI decorators:
{
provide: ACTIVE_TAB,
deps: [
[new Optional(), new Self(), RouterLinkActive],
],
useFactory: activeTabFactory,
}
Next, we attach these providers to the component:
@Component({
..
providers: [ORGANIZATION_PROVIDERS],
})
And now the component simply injects the result:
@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 becomes just one line of dependency injection.
The template stays exactly as it was:
<p *ngIf="organization$ | async as organization">
{{organization.name}} from {{organization.city}}
</p>
What does this shift buy us?
- Transparent dependencies: the component only injects what it actually uses for rendering. Nothing extra gets pulled in.
- Simpler testing: the provider’s factory is just a function, so it’s straightforward to test in isolation. And for the component, we no longer need to construct a full DI graph — just supply
ORGANIZATION_INFOwith mock data. - Effortless evolution: need the component to handle a different data source? Swap out one line. Want to adjust how data is transformed? Edit the factory. Require more data? Add another token — the providers array can hold as many as you like.
Since we started using this pattern, our components and directives have become noticeably cleaner. Dividing data preparation from data presentation makes both easier to modify and extend. Bugs become simpler to isolate too: you can zero in on whether the issue lies in the transformation or in the display.
At Jamigo.app we push this idea to its limits. If you’d like to see more writing on DI-based architecture, give this Tweet a like so my colleague Alex gets motivated to write it up ?
Wrapping up
This approach isn’t a cure-all. There’s no need to reach for providers in every tiny situation — sometimes doing the conversion in a method or relying on Angular pipes is the clearer choice.
Still, I hope private providers make your life easier when components get weighed down by dependencies, or offer a useful path when you’re gradually decomposing big chunks of logic.
