Hierarchical DI — Component-Level Providers and Scoped Services

Angular’s hierarchical injector tree means every component has its own injector that can provide services scoped to that component and its descendants. When a service is requested, Angular walks up the injector tree until it finds a provider. providedIn: 'root' services are at the top. Services in component providers: [] are scoped to that component subtree. Understanding this hierarchy lets you provide the same service class with independent instances to different parts of the component tree — essential for paginated lists, multi-step forms, and repeated UI patterns.

Component-Level Providers

// ── PaginationService — stateful, should be per-component ─────────────────
@Injectable()   // NO providedIn — must be explicitly provided
export class PaginationService {
  private _page    = signal(1);
  private _size    = signal(10);
  private _total   = signal(0);

  readonly page    = this._page.asReadonly();
  readonly size    = this._size.asReadonly();
  readonly total   = this._total.asReadonly();
  readonly pages   = computed(() => Math.ceil(this._total() / this._size()));
  readonly hasNext = computed(() => this._page() < this.pages());
  readonly hasPrev = computed(() => this._page() > 1);

  setPage(page: number):  void { this._page.set(page); }
  setTotal(total: number): void { this._total.set(total); }
  next(): void { if (this.hasNext()) this._page.update(p => p + 1); }
  prev(): void { if (this.hasPrev()) this._page.update(p => p - 1); }
  reset(): void { this._page.set(1); }
}

// ── Component providing its own PaginationService instance ────────────────
@Component({
  selector:   'app-post-list',
  standalone:  true,
  providers:  [PaginationService],   // ← scoped to this component + descendants
  imports:    [PaginationControlsComponent],
  template: `
    <!-- PaginationControlsComponent also injects PaginationService ──────── -->
    <!-- It gets the SAME instance as PostListComponent (same subtree) ─────── -->
    <app-pagination-controls />
    <div class="posts">...</div>
  `,
})
export class PostListComponent implements OnInit {
  // Gets the scoped instance (not the root singleton)
  protected pagination = inject(PaginationService);
  private   posts$     = inject(PostsService);

  ngOnInit() {
    // When pagination.page() changes, load new page
    effect(() => {
      this.posts$.loadPublished(this.pagination.page());
    });
  }
}

// ── Two PostListComponents on the same page → two independent paginators ──
// <app-post-list />  ← has its own PaginationService instance (page 1–10)
// <app-post-list />  ← has its own PaginationService instance (page 1–5)
// These are completely independent — navigating page 3 on one does not
// affect the other's page counter

// ── viewProviders — exclude projected content from provider scope ──────────
@Component({
  selector:   'app-form-wrapper',
  standalone:  true,
  // viewProviders: only visible to this component's own template children
  // (not ng-content projected children)
  viewProviders: [FormStateService],
  template: `
    <form>
      <app-title-input />  <!-- Can inject FormStateService ── -->
      <ng-content />      <!-- Projected content CANNOT inject FormStateService -->
    </form>
  `,
})
Note: When a component provides a service in its providers: [] array, that service is created when the component is created and destroyed when the component is destroyed. This lifecycle binding is what makes component-level providers ideal for stateful services (form state, pagination, wizard steps) — the state is automatically cleaned up when the user navigates away and the component is destroyed. Root services accumulate state indefinitely; component-level services are naturally scoped.
Tip: Omit providedIn from services that should only be used at the component level: @Injectable() without any configuration. This prevents the service from being tree-shaken by the root injector and makes the intention clear — this service must be explicitly provided where it is used. If it is accidentally injected without a provider in the hierarchy, Angular throws a helpful “No provider for X” error rather than silently injecting the root singleton.
Warning: The difference between providers: [] and viewProviders: [] on a component is subtle but important. providers makes the service available to the component, its view children, and its projected content (ng-content). viewProviders makes it available only to the component’s own view children — projected content cannot inject it. Use viewProviders when the service should be private to the component’s own template and not accessible to content inserted from outside.

Common Mistakes

Mistake 1 — Providing a stateful service at root when it should be per-component (shared state when it should be independent)

❌ Wrong — @Injectable({ providedIn: 'root' }) on PaginationService; all paginated lists share one page counter.

✅ Correct — @Injectable() with providers: [PaginationService] on each component that needs its own state.

Mistake 2 — Using component providers for services that need to be shared across sibling components

❌ Wrong — AuthService in component providers; each component gets its own auth state; login in one component doesn’t affect siblings.

✅ Correct — shared state belongs at root; component-scoped state belongs in component providers.

🧠 Test Yourself

Two PostListComponent instances are on the same page, each with providers: [PaginationService]. User navigates to page 3 on the first list. What page is the second list on?