Angular 20 🅰️ DI Decorators and Provider Scopes
Chapter 19 covered the basics — what a service is, how to create one, how to inject it with inject() or constructor injection, and where providedIn: 'root' fits. This chapter goes deeper into the parts of dependency injection you don’t need every day but that matter when they come up: the parameter decorators that control how Angular looks up dependencies (@Optional, @Self, @SkipSelf, @Host, @Inject), the hierarchical injector model that determines which instance a consumer gets, and the provider scopes that place services at different levels of the tree.
Key point: Angular’s injector is hierarchical. Every component has its own injector; the root injector sits at the top. When you inject a service, Angular walks up the tree from the consumer’s injector until it finds a provider. The decorators — @Optional, @Self, @SkipSelf, @Host — customize that walk. They change which injector Angular asks and what happens if nothing is found. Provider scopes decide where the service is registered in the first place. Together they give you full control over the dependency graph.
The hierarchical injector
Angular doesn’t have one injector — it has a tree.
┌──────────────────────────────────────┐
│ Platform Injector │
│ (across apps on the page) │
│ │ │
│ ▼ │
│ Root Injector │
│ (app-wide singletons) │
│ │ │
│ ▼ │
│ Module Injector (lazy modules) │
│ │ │
│ ▼ │
│ Component Injector (per component) │
│ │ │
│ ▼ │
│ Child Component Injector │
│ │
└──────────────────────────────────────┘
What each level provides:
| Level | Scope |
|---|---|
| Platform | Across multiple Angular apps on the page |
| Root | App-wide — providedIn: 'root' |
| Module | One per lazy-loaded module |
| Component | One per component instance |
| Child component | Nested — inherits parents |
When you inject a service, Angular walks up:
- Check the current component’s injector
- If not found, check its parent’s injector
- Continue up to the root injector
- If not found anywhere — error (unless
@Optional)
The first provider found wins. If the component provides UserService, that instance is used. If not, the parent’s, and so on.
A concrete tree:
@Injectable({ providedIn: 'root' })
export class ConfigService {}
// Available in the root injector
@Component({
selector: 'app-child',
providers: [LocalService]
})
export class ChildComponent {}
// LocalService is only in ChildComponent's injector
If a component injects LocalService, only its own subtree can see it. A sibling component doesn’t.
Why hierarchy matters: It lets you scope services locally. A component-scoped service belongs to that component’s instance, and its children inherit it. Siblings don’t. That’s how per-component state works without global singletons.
Why hierarchical and not flat: A flat injector would force every service to be global. That’s fine for auth or config, but it’s wrong for component state. Hierarchical injectors let each level of the tree provide what belongs at that level. A modal’s state service lives with the modal; a page’s state lives with the page; the app’s state lives at the root.
@Optional — allow missing providers
By default, if no provider is found, Angular throws. @Optional makes the dependency optional — the injected value is null if not found.
import { Component, Optional, inject } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
template: `<p>{{ message }}</p>`
})
export class GreetingComponent {
private logger = inject(LoggerService, { optional: true });
message = this.logger ? this.logger.greet() : 'Hello';
}
With inject(), the option is { optional: true }. The result is LoggerService | null.
Constructor decorator form:
constructor(@Optional() private logger: LoggerService) {}
The parameter is typed LoggerService but is actually LoggerService | null at runtime. TypeScript doesn’t know — you have to guard.
Using it safely:
ngOnInit(): void {
this.logger?.log('Component initialized'); // optional chaining
}
When @Optional is useful:
- Plugins — a service that may or may not be provided
- Testing — dependencies that are optional in unit tests
- Feature flags — a service that exists only when a feature is enabled
- Parent-provided values — a child that works with or without a parent value
Example — a component that optionally shows a header:
@Component({
selector: 'app-panel',
template: `
@if (header) {
<header>{{ header.title }}</header>
}
<ng-content></ng-content>
`
})
export class PanelComponent {
header = inject(PanelHeaderService, { optional: true });
}
If a parent provides PanelHeaderService, the panel shows a header. If not, it doesn’t.
Why @Optional matters: It lets code handle the case where a provider is missing. Without it, a missing provider is an error. With it, the dependency is a real possibility — present or absent.
Why
@Optionaldoesn’t change the type: TypeScript still seesLoggerService, but at runtime it might benull. The framework can’t change the declared type from a decorator. Always guard —?.orif (logger).
@Self — only the current injector
@Self tells Angular to look only in the current injector. If the service isn’t provided there, the injection fails.
@Component({
selector: 'app-form',
providers: [FormStateService],
template: `...`
})
export class FormComponent {
private state = inject(FormStateService, { self: true });
}
@Self skips the parent injectors. The service must be provided at the component level or higher-in-that-same-level.
Constructor decorator form:
constructor(@Self() private state: FormStateService) {}
Why @Self matters: It ensures the service is component-scoped. If someone accidentally provides it at the root, @Self still forces the component-level instance. It’s a way to say “I want my own instance, not a shared one.”
When @Self is useful:
- Component-scoped services that must not fall back to a global provider
- Avoiding accidental singleton usage
- Testing that a provider is where you expect
Example — a table that must have its own state:
@Component({
selector: 'app-data-table',
providers: [TableStateService],
template: `...`
})
export class DataTableComponent {
private state = inject(TableStateService, { self: true });
}
Even if TableStateService were also provided at the root, this component would use its own.
Failure mode:
If the service isn’t provided at the component level, @Self throws NullInjectorError — even if a provider exists at the root. That’s the point.
Why @Self combined with @Optional: You often want “my own instance, or null.” @Self() @Optional() gives that — inject only from this injector, and return null if absent.
Why
@Selfexists: Hierarchical injection is convenient but sometimes too forgiving. A component might expect its own state instance and accidentally get a shared one because a provider exists higher up.@Selfnarrows the search — only this injector counts. It’s a precision tool.
@SkipSelf — skip the current injector
@SkipSelf tells Angular to start looking at the parent injector — skip the current one.
@Component({
selector: 'app-inner',
providers: [UserService],
template: `...`
})
export class InnerComponent {
// Get the parent's UserService, not this component's
private parentUserService = inject(UserService, { skipSelf: true });
}
The component provides UserService for its own children, but injects the parent’s instance for itself.
Constructor decorator form:
constructor(@SkipSelf() private parentService: UserService) {}
The classic use case — the singleton guard:
@Injectable()
export class CoreModule {
constructor(@Optional() @SkipSelf() parent: CoreModule) {
if (parent) {
throw new Error('CoreModule is already loaded. Import it in AppModule only.');
}
}
}
This pattern ensures a module or service is only provided once. If a parent already provides it, the constructor throws.
Modern version with inject():
@Injectable({ providedIn: 'root' })
export class SingletonService {
constructor() {
const parent = inject(SingletonService, { optional: true, skipSelf: true });
if (parent) {
throw new Error('SingletonService already provided');
}
}
}
Why @SkipSelf matters: It avoids infinite recursion. If a component provides UserService and also injects UserService without skipping, it would inject itself — and if the injection is what triggers the provider, that’s a loop. @SkipSelf breaks it by looking up.
Other uses:
- Decorator pattern — the wrapper injects the wrapped service from the parent
- Override detection — checking whether a parent already provides something
- Shared services — a component provides a service for its subtree, but itself uses the shared one
Example — wrapper that augments a service:
@Injectable()
export class CachingUserService {
private base = inject(UserService, { skipSelf: true });
private cache = new Map<number, User>();
getUser(id: number): User {
if (!this.cache.has(id)) {
this.cache.set(id, this.base.getUser(id));
}
return this.cache.get(id)!;
}
}
The caching service wraps the base service. @SkipSelf gets the underlying one — otherwise it would inject itself.
Why
@SkipSelfis idiomatic for wrappers: A wrapper provides a token and injects the same token. Without@SkipSelf, the wrapper injects itself — infinite recursion or the wrong instance.@SkipSelfsays “give me the one from above.” It’s the standard way to compose services.
@Host — stop at the host boundary
@Host limits the injector walk to the host component’s injector. It stops before crossing into parent components.
@Component({
selector: 'app-child',
template: `...`
})
export class ChildComponent {
private state = inject(ModalStateService, { host: true });
}
@Host looks up the injector tree but stops at the boundary of the current component’s host — the closest ancestor component that provides the service within the view boundary. It doesn’t go beyond the host into the parent’s parent.
Constructor decorator form:
constructor(@Host() private state: ModalStateService) {}
When @Host is used:
- Directives that depend on their host component — a directive attached to
<app-modal>may want the modal’s state - Content projection — child content that needs the host’s context
- Isolation from ancestors — a component that should only see providers up to its host
A practical example — a directive on a component:
@Directive({
selector: '[appModalClose]'
})
export class ModalCloseDirective {
private modal = inject(ModalComponent, { host: true });
@HostListener('click')
close(): void {
this.modal.close();
}
}
The directive finds the ModalComponent it’s attached to. @Host limits the search to the directive’s host — the closest component boundary.
The difference from plain injection: Without @Host, Angular would search all the way to the root. With @Host, it stops at the host component. If the service isn’t found by then, the injection fails.
Why @Host matters: In component-based apps, some dependencies logically belong to the enclosing component — not to the whole app. @Host enforces that boundary. It’s used heavily in component libraries and directives.
Interaction with @Self:
@Self— only the current injector@Host— current injector and up to the host component- Neither — walk to the root
When to combine:
constructor(@Host() @Optional() private state: StateService) {}
Look only up to the host, but allow the value to be absent.
Why
@Hostand not just@Self: A directive’s injector is not the same as its host component’s injector. The directive is attached to a component, and the component’s providers are one level up.@Hostsays “give me what my host provides, not what I provide.” It’s the right scope for directives that interact with their host.
@Inject — use a custom token
@Inject specifies the token to inject, rather than relying on the parameter type.
import { Inject, InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
@Component({ /* ... */ })
export class ApiComponent {
constructor(@Inject(API_URL) private apiUrl: string) {}
}
@Inject(API_URL) says “inject whatever is provided for the API_URL token.” The parameter type is string, but the token is the InjectionToken.
Why it’s needed: Angular infers the token from the parameter’s type by default. For a class type, that works. For an InjectionToken, the type is string or an interface — not the token itself. @Inject specifies the token explicitly.
With inject():
private apiUrl = inject(API_URL);
inject() takes the token directly. No @Inject needed.
Combining with other decorators:
constructor(
@Inject(API_URL) private url: string,
@Optional() @Inject(LOGGER) private logger: Logger | null
) {}
@Inject sets the token; @Optional sets the behavior.
When @Inject is used:
- Injecting an
InjectionToken - Injecting a value under a class token that differs from the parameter type
- Migrating from older Angular code
Why @Inject exists: Parameter types aren’t always the token. InjectionToken<string> doesn’t have a class to use as the token — it’s the token itself. @Inject lets you specify it. Without it, Angular would try to use String (the built-in type) as the token, which fails.
Why
inject()skips@Inject:inject(TOKEN)takes the token as its argument, so there’s no ambiguity.@Injectexists for constructor injection, where the token can’t be inferred from the type. Modern code usesinject()and avoids@Injectentirely.
Provider scopes in the tree
Where a service is provided determines who shares it.
Root scope:
@Injectable({ providedIn: 'root' })
export class UserService {}
One instance for the whole app.
Module scope (legacy):
@NgModule({
providers: [UserService]
})
export class UserModule {}
One instance per module injector. Lazy-loaded modules get their own.
Component scope:
@Component({
providers: [FormStateService],
// ...
})
export class FormComponent {}
One instance per component instance. Children can inject it; siblings can’t.
Route scope:
const routes: Routes = [
{
path: 'users',
component: UserListComponent,
providers: [UserFilterService]
}
];
One instance per route activation. The service is available to the routed component and its children.
Visualizing the scopes:
┌──────────────────────────────────────────────┐
│ Root: providedIn: 'root' │
│ │ │
│ ├── Available everywhere │
│ │ │
├──────────────────────────────────────────────┤
│ Module: @NgModule providers │
│ │ │
│ ├── Available in the module │
│ │ │
├──────────────────────────────────────────────┤
│ Component: @Component providers │
│ │ │
│ ├── Available in this component │
│ │ and its children │
│ │ │
├──────────────────────────────────────────────┤
│ Route: providers in Route config │
│ │ │
│ ├── Available in the routed view │
│ │
└──────────────────────────────────────────────┘
When to use each:
| Scope | Use for |
|---|---|
| Root | Auth, config, logging, shared state |
| Module | Feature-wide state (legacy apps) |
| Component | Per-component state |
| Route | Per-route state |
Effect of lazy loading: A service provided in a lazy-loaded module gets a new instance when the module loads. If you want a singleton across all lazy modules, use providedIn: 'root'.
Why scopes matter for state: Two components that inject a root-scoped service see the same instance — shared state. Two components that inject a component-scoped service get separate instances — isolated state. The scope is what determines behavior.
Why root is the default: Most services are shared — auth, config, API clients. Root scope gives one instance everywhere. Component and route scopes are for state that’s specific to a subtree. Choosing the right scope is choosing the right data model — global vs local.
Scope resolution and walking the tree
Understanding how Angular finds a provider explains the decorators.
Without decorators — full walk:
Consumer's injector
▼ (not found)
Parent's injector
▼ (not found)
...
▼ (not found)
Root injector
▼ (not found)
Platform injector
▼ (not found)
Error: NullInjectorError
Angular walks up until it finds a provider or hits the top.
With @Self — only the current injector:
Consumer's injector
▼ (found → use it)
▼ (not found → error)
With @SkipSelf — start one level up:
Parent's injector
▼ (not found)
...
▼
Root injector
With @Host — stop at the host boundary:
Consumer's injector
▼ (not found)
Host component's injector
▼ (found → use it)
▼ (not found → error, doesn't go further)
With @Optional — missing is allowed:
Consumer's injector
▼ (not found)
...
▼ (not found anywhere)
null instead of error
Combining: Decorators can be combined. @Self() @Optional() looks only at the current injector and returns null if not found.
private state = inject(StateService, { self: true, optional: true });
Why this matters: The decorators change where Angular looks and what happens when nothing is found. Knowing the walk explains every decorator’s behavior. @Self and @SkipSelf change the starting point; @Host changes the stopping point; @Optional changes the failure mode.
Why decorators aren’t just conveniences: They’re semantic.
@Selfisn’t “a shorter way to say something” — it means “I need the local instance, not a shared one.”@SkipSelfmeans “give me my parent’s.” The decorators express intent about the dependency graph. Getting them wrong gives the wrong instance, not just a longer path.
A full example
A component library with all the decorators in play.
// ============================================
// TOKENS AND SERVICES
// ============================================
import { Injectable, InjectionToken } from '@angular/core';
export const THEME = new InjectionToken<{ color: string }>('THEME');
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(msg: string): void { console.log(msg); }
}
@Injectable()
export class LocalStateService {
private values = new Map<string, unknown>();
set(key: string, value: unknown): void {
this.values.set(key, value);
}
get(key: string): unknown {
return this.values.get(key);
}
}
// ============================================
// MODAL COMPONENT WITH HOST CONTEXT
// ============================================
import { Component, inject, contentChild } from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
providers: [LocalStateService],
template: `
<div class="modal">
<header>{{ title }}</header>
<main><ng-content></ng-content></main>
<footer><ng-content select="[close]"></ng-content></footer>
</div>
`
})
export class ModalComponent {
title = '';
state = inject(LocalStateService); // own instance
}
// ============================================
// DIRECTIVE THAT USES HOST
// ============================================
import { Directive, HostListener, inject } from '@angular/core';
@Directive({
selector: '[appModalClose]',
standalone: true
})
export class ModalCloseDirective {
// @Host — find the ModalComponent in the host boundary
private modal = inject(ModalComponent, { host: true });
@HostListener('click')
close(): void {
this.modal.state.set('closed', true);
}
}
// ============================================
// SERVICE THAT WRAPS ANOTHER
// ============================================
@Injectable()
export class CachingLogger {
// @SkipSelf — use the root logger, not self
private base = inject(LoggerService, { skipSelf: true });
private lines: string[] = [];
log(msg: string): void {
this.lines.push(msg);
this.base.log(`[cached] ${msg}`);
}
getLines(): string[] { return [...this.lines]; }
}
// ============================================
// COMPONENT WITH OPTIONAL DEPENDENCY
// ============================================
@Component({
selector: 'app-panel',
standalone: true,
providers: [CachingLogger],
template: `<p>{{ message }}</p>`
})
export class PanelComponent {
// @Optional — allow missing theme
private theme = inject(THEME, { optional: true });
private logger = inject(CachingLogger); // own instance
message = this.theme
? `Theme: ${this.theme.color}`
: 'No theme provided';
}
// ============================================
// COMPONENT WITH SELF-SCOPED STATE
// ============================================
@Component({
selector: 'app-form',
standalone: true,
providers: [LocalStateService],
template: `...`
})
export class FormComponent {
// @Self — own instance, not shared
private state = inject(LocalStateService, { self: true });
save(): void {
this.state.set('saved', true);
}
}
// ============================================
// ROOT PROVIDERS
// ============================================
import { bootstrapApplication } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
{ provide: THEME, useValue: { color: '#3b82f6' } }
]
});
What this shows:
THEME— anInjectionTokenprovided at the root@Optional— panel works with or without a theme@Self— form’s state is its own instance@Host— directive finds the modal component it’s attached to@SkipSelf— caching logger uses the root logger
Each decorator solves a specific problem. Together they cover the situations where the default injector walk isn’t right.
Why this shape: It’s a realistic component library. Theme is app-wide, modal state is per-modal, the directive talks to its host, the wrapper delegates to the shared logger. The decorators are what make each dependency land in the right place.
Complete Example Session
# ============================================
# PART 1: @Optional
# ============================================
cat > optional.ts << 'EOF'
import { Component, inject, InjectionToken } from '@angular/core';
const GREETING = new InjectionToken<string>('GREETING');
@Component({
selector: 'app-greeting',
standalone: true,
template: `<p>{{ message }}</p>`
})
export class GreetingComponent {
private greeting = inject(GREETING, { optional: true });
message = this.greeting ?? 'Hello (no greeting provided)';
}
EOF
npx tsc --noEmit optional.ts
# (no errors)
# ============================================
# PART 2: @Self
# ============================================
cat > self.ts << 'EOF'
import { Component, inject, Injectable } from '@angular/core';
@Injectable()
export class FormState {
value = '';
}
@Component({
selector: 'app-form',
standalone: true,
providers: [FormState],
template: `...`
})
export class FormComponent {
// @Self — own instance, not a parent's
state = inject(FormState, { self: true });
}
EOF
npx tsc --noEmit self.ts
# (no errors)
# ============================================
# PART 3: @SkipSelf
# ============================================
cat > skipself.ts << 'EOF'
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Logger {
log(msg: string): void { console.log(msg); }
}
@Injectable()
export class CachingLogger {
// @SkipSelf — get the parent Logger, not self
private base = inject(Logger, { skipSelf: true });
private lines: string[] = [];
log(msg: string): void {
this.lines.push(msg);
this.base.log(`[cache] ${msg}`);
}
}
EOF
npx tsc --noEmit skipself.ts
# (no errors)
# ============================================
# PART 4: @Host
# ============================================
cat > host.ts << 'EOF'
import { Component, Directive, HostListener, inject } from '@angular/core';
@Component({
selector: 'app-modal',
standalone: true,
template: `<div class="modal"><ng-content></ng-content></div>`
})
export class ModalComponent {
close(): void { console.log('closed'); }
}
@Directive({
selector: '[appModalClose]',
standalone: true
})
export class ModalCloseDirective {
// @Host — find the ModalComponent in the host
private modal = inject(ModalComponent, { host: true });
@HostListener('click')
onClick(): void {
this.modal.close();
}
}
EOF
npx tsc --noEmit host.ts
# (no errors)
# ============================================
# PART 5: @Optional + @SkipSelf
# ============================================
cat > combo.ts << 'EOF'
import { Injectable, inject, Optional, SkipSelf } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class SingletonService {
constructor() {
// Singleton guard — throw if already provided
const parent = inject(SingletonService, {
optional: true,
skipSelf: true
});
if (parent) {
throw new Error('SingletonService already provided');
}
}
}
EOF
npx tsc --noEmit combo.ts
# (no errors)
# ============================================
# PART 6: ERROR CASE
# ============================================
cat > missing.ts << 'EOF'
import { Component, inject, InjectionToken } from '@angular/core';
const REQUIRED = new InjectionToken<string>('REQUIRED');
@Component({
selector: 'app-broken',
standalone: true,
template: `...`
})
export class BrokenComponent {
// Without optional — throws if not provided
private value = inject(REQUIRED);
}
EOF
npx tsc --noEmit missing.ts
# (no errors)
# (runtime error if not provided)
rm missing.ts
# ============================================
# PART 7: SUMMARY
# ============================================
cat << 'EOF'
Decorator effects:
(no decorator) → walk to root; error if not found
@Optional → walk to root; null if not found
@Self → current injector only; error if not found
@SkipSelf → start at parent; walk to root
@Host → stop at host component boundary
@Inject(T) → use token T instead of inferred type
Combos:
@Self + @Optional → own or null
@Host + @Optional → host or null
@SkipSelf + @Optional → parent or null
@Inject + @Optional → token or null
EOF
Quick Reference
Parameter Decorators
| Decorator | Behavior |
|---|---|
@Optional() | Value can be null |
@Self() | Only current injector |
@SkipSelf() | Start at parent |
@Host() | Stop at host component |
@Inject(TOKEN) | Use custom token |
inject() Options
| Option | Equivalent |
|---|---|
{ optional: true } | @Optional |
{ self: true } | @Self |
{ skipSelf: true } | @SkipSelf |
{ host: true } | @Host |
inject(TOKEN) | @Inject(TOKEN) |
Injector Hierarchy
| Level | Scope |
|---|---|
| Platform | Across apps |
| Root | App-wide |
| Module | Per module (lazy) |
| Route | Per route |
| Component | Per instance |
| Child component | Inherits parent |
Walk Behavior
| Decorator | Start | Stop | Missing |
|---|---|---|---|
| (none) | Current | Root | Error |
@Optional | Current | Root | null |
@Self | Current | Current | Error |
@SkipSelf | Parent | Root | Error |
@Host | Current | Host | Error |
@Optional @Self | Current | Current | null |
@Optional @SkipSelf | Parent | Root | null |
Provider Scopes
| Scope | Register with | Instance count |
|---|---|---|
| Root | providedIn: 'root' | One per app |
| Platform | providedIn: 'platform' | One per page |
| Module | @NgModule providers | One per module |
| Route | Route providers | One per route |
| Component | @Component providers | One per instance |
When to Use Each Decorator
| Decorator | Use when |
|---|---|
@Optional | Dependency may be absent |
@Self | Need this component’s own instance |
@SkipSelf | Wrapping another service |
@Host | Directive needs host component |
@Inject(TOKEN) | Token is not a class |
Common Patterns
| Pattern | Decorators |
|---|---|
| Singleton guard | @Optional @SkipSelf |
| Wrapper service | @SkipSelf |
| Directive on component | @Host |
| Optional feature | @Optional |
| Non-class token | @Inject(TOKEN) |
| Component-scoped state | @Self |
Combination Reference
| Combination | Result |
|---|---|
@Optional @Self | Own instance or null |
@Optional @SkipSelf | Parent instance or null |
@Optional @Host | Host instance or null |
@Self @Inject(T) | Token from current injector |
@Host @Optional | Host context, may be missing |
inject() Field Options
inject(Service) // required, walk to root
inject(Service, { optional: true }) // or null
inject(Service, { self: true }) // current injector
inject(Service, { skipSelf: true }) // parent
inject(Service, { host: true }) // host boundary
inject(Service, { self: true, optional: true }) // self or null
inject(TOKEN) // custom token
Runtime Errors
| Error | Cause |
|---|---|
NullInjectorError: No provider for X | Not provided and not optional |
No provider for X in Self | @Self but not provided locally |
No provider for X in Host | @Host but host doesn’t provide |
Scope Effects on State
| Scope | Shared? |
|---|---|
| Root | ✅ All consumers |
| Module | ⚠️ Within the module |
| Route | ⚠️ Within the route |
| Component | ❌ Per instance |
| Self | ❌ Own injector only |
Best Practices
✅ Do This:
// Use @Optional for genuinely optional dependencies
private logger = inject(LoggerService, { optional: true }); // ✅
// Guard optional dependencies
this.logger?.log('message'); // ✅
// Use @Self for component-scoped services
state = inject(FormState, { self: true }); // ✅
// Use @SkipSelf when wrapping
private base = inject(Logger, { skipSelf: true }); // ✅
// Use @Host in directives attached to components
private modal = inject(ModalComponent, { host: true }); // ✅
// Use inject(TOKEN) instead of @Inject
private url = inject(API_URL); // ✅
// Combine @Optional with @Self or @SkipSelf
inject(Service, { self: true, optional: true }); // ✅
// Provide component-scoped services with the component
@Component({ providers: [FormState] }) // ✅
// Use route-scoped services for per-route state
{ path: 'x', providers: [StateService], component: X } // ✅
❌ Don’t Do This:
// Don't use @Optional when the dependency is required
private s = inject(Service, { optional: true });
this.s.method(); // ❌ null reference if missing // ❌
// Don't use @Self without ensuring the provider exists
inject(Service, { self: true }); // ⚠️ errors if not provided // ⚠️
// Don't use @SkipSelf without a parent provider
inject(Service, { skipSelf: true }); // ⚠️ errors if no parent // ⚠️
// Don't use @Host where the service lives at the root
inject(Service, { host: true }); // ⚠️ errors // ⚠️
// Don't use @Inject with a class type
constructor(@Inject(UserService) s: UserService) // ⚠️ // ⚠️
// Don't overuse decorators
// Most injection uses defaults // ⚠️
// Don't forget to import Optional, Self, etc.
import { Optional, Self } from '@angular/core'; // ✅
// Don't mix decorators and inject() for the same dependency
constructor(@Self() s: S) { }
// and then inject(S) again // ⚠️ duplicate // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
@Optional without guard | Null reference | Use ?. or if |
@Self without provider | Runtime error | Add component provider |
@SkipSelf at root | Runtime error | Ensure parent provider |
@Host beyond host | Runtime error | Provide at host |
@Inject with class | Unnecessary | Use type or inject() |
| Missing import | Compile error | Import from @angular/core |
| Mixing styles | Confusing | Prefer inject() |
| Wrong scope for state | Wrong instance | Choose scope deliberately |
| Lazy module scoping | Duplicate instances | Use root for singletons |
| Component-scoped state shared | Wrong instance count | Use @Self or component providers |
Real-World Examples
1. Optional logger
private logger = inject(Logger, { optional: true });
2. Guard optional value
this.logger?.log('msg');
3. Self-scoped state
state = inject(State, { self: true });
4. Component provides self-scoped service
@Component({ providers: [State] })
5. SkipSelf wrapper
private base = inject(Logger, { skipSelf: true });
6. Singleton guard
constructor() {
const parent = inject(Self, { optional: true, skipSelf: true });
if (parent) throw new Error('Already provided');
}
7. Host-bound directive
private modal = inject(ModalComponent, { host: true });
8. Optional self
state = inject(State, { self: true, optional: true });
9. Injection token with inject()
private url = inject(API_URL);
10. @Inject in constructor
constructor(@Inject(API_URL) private url: string) {}
11. Route-scoped service
{ path: 'x', providers: [State], component: X }
12. Component-scoped service
@Component({ providers: [State] })
13. Root-scoped
@Injectable({ providedIn: 'root' })
14. Platform-scoped
@Injectable({ providedIn: 'platform' })
15. Module-scoped (legacy)
@NgModule({ providers: [State] })
16. Null-safe optional access
if (this.theme) { this.theme.apply(); }
17. Multiple decorators in constructor
constructor(
@Optional() @SkipSelf() parent: Self
) {}
18. Combining with inject()
const parent = inject(Self, { optional: true, skipSelf: true });
19. Cache wrapper
@Injectable()
export class Cache {
private base = inject(Api, { skipSelf: true });
}
20. Directive on host component
@Directive({ selector: '[appClose]' })
export class CloseDirective {
private dialog = inject(DialogComponent, { host: true });
}
Visual: Injector Tree Walk
┌──────────────────────────────────────────────┐
│ Root Injector │
│ ┌────────────────────────────────────────┐ │
│ │ LoggerService (providedIn: 'root') │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
▲
│ walk up if not found
│
┌──────────────────────────────────────────────┐
│ Parent Component Injector │
│ (no LoggerService here) │
│ │
└──────────────────────────────────────────────┘
▲
│ walk up
│
┌──────────────────────────────────────────────┐
│ Child Component Injector │
│ (consumer) │
│ │
│ inject(LoggerService) │
│ │ │
│ ▼ │
│ Not here → walk up │
│ │ │
│ ▼ │
│ Parent → not here → walk up │
│ │ │
│ ▼ │
│ Root → found ✅ │
│ │
└──────────────────────────────────────────────┘
Visual: @Self
┌──────────────────────────────────────────────┐
│ Root Injector │
│ (has LoggerService — but ignored) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Component Injector │
│ providers: [LoggerService] │
│ │
│ @Self inject(LoggerService) │
│ │ │
│ ▼ │
│ Found here ✅ — does not walk up │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ @Self without provider: │
│ │
│ Component Injector │
│ (no LoggerService) │
│ │ │
│ ▼ │
│ Error — does not walk to root ❌ │
│ │
└──────────────────────────────────────────────┘
Visual: @SkipSelf
┌──────────────────────────────────────────────┐
│ Root Injector │
│ ┌────────────────────────────────────────┐ │
│ │ LoggerService │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
▲
│ found here
│
┌──────────────────────────────────────────────┐
│ Parent Injector │
│ │
└──────────────────────────────────────────────┘
▲
│ start here
│
┌──────────────────────────────────────────────┐
│ Consumer Injector │
│ providers: [LoggerService] ← skipped │
│ │
│ @SkipSelf inject(LoggerService) │
│ │ │
│ ▼ │
│ Start at parent → walk up → root │
│ │ │
│ ▼ │
│ Use root LoggerService ✅ │
│ │
└──────────────────────────────────────────────┘
Visual: @Host
┌──────────────────────────────────────────────┐
│ Root Injector │
│ (ModalComponent is NOT here) │
│ │
└──────────────────────────────────────────────┘
▲
│ blocked
│
┌──────────────────────────────────────────────┐
│ Parent Component Injector │
│ (not the host) │
│ │
└──────────────────────────────────────────────┘
▲
│ blocked
│
┌──────────────────────────────────────────────┐
│ Host Component Injector (ModalComponent) │
│ ┌────────────────────────────────────────┐ │
│ │ ModalComponent │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Directive │ │
│ │ │ │
│ │ @Host inject(ModalComponent) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Found at host ✅ │ │
│ │ Stops walking up │ │
│ └──────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
Visual: @Optional
┌──────────────────────────────────────────────┐
│ Without @Optional │
│ │
│ inject(Service) │
│ │ │
│ ▼ │
│ Not found anywhere → Error ❌ │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With @Optional │
│ │
│ inject(Service, { optional: true }) │
│ │ │
│ ▼ │
│ Not found anywhere → null ✅ │
│ │
│ Then: this.service?.method() │
│ │
└──────────────────────────────────────────────┘
Visual: Provider Scopes
┌──────────────────────────────────────────────┐
│ Platform │
│ ─ providedIn: 'platform' │
│ ─ Across apps on page │
│ │
├──────────────────────────────────────────────┤
│ Root │
│ ─ providedIn: 'root' │
│ ─ App-wide singleton │
│ │
├──────────────────────────────────────────────┤
│ Lazy module │
│ ─ @NgModule providers │
│ ─ New instance on lazy load │
│ │
├──────────────────────────────────────────────┤
│ Route │
│ ─ Route providers │
│ ─ New instance per navigation │
│ │
├──────────────────────────────────────────────┤
│ Component │
│ ─ @Component providers │
│ ─ New instance per component │
│ │
└──────────────────────────────────────────────┘
Visual: Singleton Guard Pattern
┌──────────────────────────────────────────────┐
│ @Injectable({ providedIn: 'root' }) │
│ export class SingletonService { │
│ constructor() { │
│ const parent = inject( │
│ SingletonService, │
│ { optional: true, skipSelf: true } │
│ ); │
│ if (parent) { │
│ throw new Error('Already provided'); │
│ } │
│ } │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ What happens: │
│ │
│ First injection → no parent → OK │
│ │
│ Someone provides it again at component → │
│ the constructor runs, finds parent, throws │
│ │
│ Protects against duplicate providers │
│ │
└──────────────────────────────────────────────┘
Visual: Wrapper Service
┌──────────────────────────────────────────────┐
│ Without @SkipSelf │
│ │
│ @Injectable() │
│ export class CachingLogger { │
│ private base = inject(Logger); │
│ // ↑ │
│ // injects itself → infinite loop │
│ } │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With @SkipSelf │
│ │
│ @Injectable() │
│ export class CachingLogger { │
│ private base = inject(Logger, { │
│ skipSelf: true │
│ }); │
│ // ↑ │
│ // injects the parent Logger │
│ // (from root or another level) │
│ } │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Dependency may be missing? │
│ │ │
│ └── Yes ──► @Optional │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need this component's own instance? │
│ │ │
│ └── Yes ──► @Self │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Wrapping another service? │
│ │ │
│ └── Yes ──► @SkipSelf │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Directive needs its host component? │
│ │ │
│ └── Yes ──► @Host │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Token is not a class? │
│ │ │
│ └── Yes ──► inject(TOKEN) │
│ │
└──────────────────────────────────────────────┘
Summary
| Decorator | Effect |
|---|---|
@Optional | Allow missing — value is null |
@Self | Only current injector |
@SkipSelf | Start at parent injector |
@Host | Stop at host component |
@Inject(T) | Use token T |
| (none) | Walk to root |
| Scope | Register | Instance |
|---|---|---|
| Root | providedIn: 'root' | One per app |
| Platform | providedIn: 'platform' | One per page |
| Module | @NgModule providers | One per module |
| Route | Route providers | One per route |
| Component | @Component providers | One per instance |
Key takeaways:
- Angular’s injector is hierarchical — component, module, root, platform
- Injection walks up the tree until it finds a provider
@Optionalallows a missing provider — the value isnull@Selflimits the search to the current injector@SkipSelfstarts at the parent — the standard pattern for wrappers@Hoststops at the host component boundary@Inject(TOKEN)specifies a custom token — needed forInjectionTokeninject()options —{ optional, self, skipSelf, host }— replace the decorators- Provider scopes — root, module, route, component — determine instance count
- The singleton guard uses
@Optional @SkipSelfto detect duplicate providers - Wrapper services use
@SkipSelfto inject the wrapped service - Directives on components use
@Hostto reach their host component
Remember: Most injection uses the defaults — walk up, find the service, error if missing. The decorators are for the cases where the default isn’t right. @Optional when the dependency may be absent, @Self when you need the local instance, @SkipSelf when you’re wrapping, @Host when you need your host, and @Inject when the token isn’t a class. Provider scopes decide where services live in the tree; the decorators control how they’re found. Together they give you full control over the dependency graph.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!