| |

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:

LevelScope
PlatformAcross multiple Angular apps on the page
RootApp-wide — providedIn: 'root'
ModuleOne per lazy-loaded module
ComponentOne per component instance
Child componentNested — inherits parents

When you inject a service, Angular walks up:

  1. Check the current component’s injector
  2. If not found, check its parent’s injector
  3. Continue up to the root injector
  4. 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 @Optional doesn’t change the type: TypeScript still sees LoggerService, but at runtime it might be null. The framework can’t change the declared type from a decorator. Always guard — ?. or if (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 @Self exists: 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. @Self narrows 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 @SkipSelf is 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. @SkipSelf says “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 @Host and 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. @Host says “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. @Inject exists for constructor injection, where the token can’t be inferred from the type. Modern code uses inject() and avoids @Inject entirely.


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:

ScopeUse for
RootAuth, config, logging, shared state
ModuleFeature-wide state (legacy apps)
ComponentPer-component state
RoutePer-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. @Self isn’t “a shorter way to say something” — it means “I need the local instance, not a shared one.” @SkipSelf means “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 — an InjectionToken provided 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

DecoratorBehavior
@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

OptionEquivalent
{ optional: true }@Optional
{ self: true }@Self
{ skipSelf: true }@SkipSelf
{ host: true }@Host
inject(TOKEN)@Inject(TOKEN)

Injector Hierarchy

LevelScope
PlatformAcross apps
RootApp-wide
ModulePer module (lazy)
RoutePer route
ComponentPer instance
Child componentInherits parent

Walk Behavior

DecoratorStartStopMissing
(none)CurrentRootError
@OptionalCurrentRootnull
@SelfCurrentCurrentError
@SkipSelfParentRootError
@HostCurrentHostError
@Optional @SelfCurrentCurrentnull
@Optional @SkipSelfParentRootnull

Provider Scopes

ScopeRegister withInstance count
RootprovidedIn: 'root'One per app
PlatformprovidedIn: 'platform'One per page
Module@NgModule providersOne per module
RouteRoute providersOne per route
Component@Component providersOne per instance

When to Use Each Decorator

DecoratorUse when
@OptionalDependency may be absent
@SelfNeed this component’s own instance
@SkipSelfWrapping another service
@HostDirective needs host component
@Inject(TOKEN)Token is not a class

Common Patterns

PatternDecorators
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

CombinationResult
@Optional @SelfOwn instance or null
@Optional @SkipSelfParent instance or null
@Optional @HostHost instance or null
@Self @Inject(T)Token from current injector
@Host @OptionalHost 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

ErrorCause
NullInjectorError: No provider for XNot 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

ScopeShared?
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

PitfallProblemSolution
@Optional without guardNull referenceUse ?. or if
@Self without providerRuntime errorAdd component provider
@SkipSelf at rootRuntime errorEnsure parent provider
@Host beyond hostRuntime errorProvide at host
@Inject with classUnnecessaryUse type or inject()
Missing importCompile errorImport from @angular/core
Mixing stylesConfusingPrefer inject()
Wrong scope for stateWrong instanceChoose scope deliberately
Lazy module scopingDuplicate instancesUse root for singletons
Component-scoped state sharedWrong instance countUse @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

DecoratorEffect
@OptionalAllow missing — value is null
@SelfOnly current injector
@SkipSelfStart at parent injector
@HostStop at host component
@Inject(T)Use token T
(none)Walk to root
ScopeRegisterInstance
RootprovidedIn: 'root'One per app
PlatformprovidedIn: 'platform'One per page
Module@NgModule providersOne per module
RouteRoute providersOne per route
Component@Component providersOne per instance

Key takeaways:

  • Angular’s injector is hierarchical — component, module, root, platform
  • Injection walks up the tree until it finds a provider
  • @Optional allows a missing provider — the value is null
  • @Self limits the search to the current injector
  • @SkipSelf starts at the parent — the standard pattern for wrappers
  • @Host stops at the host component boundary
  • @Inject(TOKEN) specifies a custom token — needed for InjectionToken
  • inject() options{ optional, self, skipSelf, host } — replace the decorators
  • Provider scopes — root, module, route, component — determine instance count
  • The singleton guard uses @Optional @SkipSelf to detect duplicate providers
  • Wrapper services use @SkipSelf to inject the wrapped service
  • Directives on components use @Host to 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!