| |

Angular 58 🅰️ Advanced Injection Scopes and Lifecycle

The previous chapter covered the provider recipes — the four use* types, the multi-providers, the provideX functions. This chapter covers where the providers live and how long they live. A provider’s scope is the part of the application that resolves the token to that provider, and a provider’s lifecycle is when the instance is created and destroyed. The two are connected: the scope determines the lifetime, and the lifetime determines the state. A root service lives for the application, a route service lives for the route, a component service lives for the component. The scope is the design decision, and the lifetime is its consequence. This chapter covers the scopes — the root, the route, the component, the directive, the element — the lifetimes that go with them, the component’s ngOnDestroy and the service’s ngOnDestroy, the DestroyRef, the takeUntilDestroyed, the pattern for the stateful service, the pattern for the scoped cache, the pattern for the cleanup, and the pitfalls that come from the wrong scope.

Key point: The root scope is the application’s lifetime, and the providedIn: 'root' is its declaration. The route scope is the route’s lifetime, and the route’s providers array is its declaration. The component scope is the component’s lifetime, and the component’s providers array is its declaration. The directive scope is the directive’s, and the element scope is the element’s. The DestroyRef is the injectable that signals the destruction, and the takeUntilDestroyed is the operator that completes the Observable when the scope is destroyed. The service’s ngOnDestroy runs when the injector that created it is destroyed, and the component’s ngOnDestroy runs when the component is destroyed. The scope determines the lifetime, and the lifetime determines the state.


The scope and the lifetime

A provider’s scope is the part of the application where it is resolved. A provider’s lifetime is the time between its creation and its destruction. The two are connected: the scope determines the injector, and the injector determines the lifetime.

The root scope. The root injector is the application’s, and the root’s service’s lifetime is the application’s. The service is created when the first injection happens, and destroyed when the application is destroyed.

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly users = signal<User[]>([]);
  load(): void { /* the load */ }
}

The UserService is the root’s, and the instance is the singleton. The state is the application’s, and the state is the shared.

Why the root’s scope is the singleton. The root’s injector is the single, and the service is the single. The two are the pair, and the pair is the shared. The root’s service is the application’s, and the application’s is the lifetime.

The route scope. The route injector is the route’s, and the route’s service’s lifetime is the route’s. The service is created when the route is loaded, and destroyed when the route is unloaded.

const routes: Routes = [
  {
    path: 'admin',
    providers: [AdminService],
    children: [{ path: '', component: AdminComponent }],
  },
];

The AdminService is the route’s, and the instance is the route’s singleton. The state is the route’s, and the state is the feature’s.

Why the route’s scope is the feature’s. The route’s injector is the feature’s, and the service is the feature’s. The two are the pair, and the pair is the scope. The route’s service is the lazy’s, and the lazy’s is the cleanup.

The component scope. The component’s injector is the component’s, and the component’s service’s lifetime is the component’s. The service is created with the component, and destroyed with the component.

@Component({
  selector: 'app-feature',
  providers: [FeatureService],
})
export class FeatureComponent {}

The FeatureService is the component’s, and the instance is the component’s. The state is the component’s, and the state is the local.

Why the component’s scope is the local. The component’s injector is the local, and the service is the local. The two are the pair, and the pair is the local. The component’s service is the subtree’s, and the subtree’s is the scope.

The directive scope. The directive’s injector is the directive’s, and the directive’s service’s lifetime is the directive’s. The pattern is the same as the component’s, and the directive’s is the element’s.

The element scope. The element’s injector is the element’s, and the element’s service’s lifetime is the element’s. The pattern is the same, and the element’s is the DOM’s.

Why the scope matters. The scope determines the lifetime, and the lifetime determines the state. The state’s scope is the design, and the design is the decision. The wrong scope is the state’s leak, and the leak is the bug.

Why the lifetime matters. The lifetime is the create and destroy, and the two are the pair. The create is the instance’s, and the destroy is the cleanup’s. The two are the pair, and the pair is the lifecycle.

Why the scope’s choice is the design. The root’s scope is the application’s, the route’s is the feature’s, and the component’s is the local. The three are the choice, and the choice is the design. The state’s scope is the design’s, and the design’s is the application’s.


The service’s ngOnDestroy

The service’s ngOnDestroy runs when the injector that created it is destroyed. The OnDestroy interface is the contract, and the method is the cleanup.

@Injectable({ providedIn: 'root' })
export class WebsocketService implements OnDestroy {
  private readonly socket = new WebSocket('wss://example.com');

  ngOnDestroy(): void {
    this.socket.close();
  }
}

The WebsocketService implements the OnDestroy, and the ngOnDestroy closes the socket. The method runs when the application is destroyed, and the application’s is the root’s.

Why the service’s ngOnDestroy matters. The service’s ngOnDestroy is the cleanup, and the cleanup is the resource’s. The socket, the timer, the subscription are the resources, and the resources need the release. The two are the pair, and the pair is the cleanup.

Why the service’s ngOnDestroy‘s timing. The service’s ngOnDestroy runs when the injector is destroyed, and the injector is the service’s scope. The root’s injector is the application’s, and the route’s is the route’s. The two are the pair, and the pair is the scope.

Why the service’s ngOnDestroy is the OnDestroy‘s. The OnDestroy is the interface, and the interface is the contract. The Angular calls the ngOnDestroy when the injector is destroyed, and the call is the cleanup. The two are the pair, and the pair is the contract.

Why the service’s ngOnDestroy should be the guarded. The ngOnDestroy should be the guarded, and the guard is the try/catch. The cleanup’s error is the swallow’s, and the swallow is the safety. The two are the pair, and the pair is the discipline.

Why the service’s ngOnDestroy can be the async. The ngOnDestroy can be the async, and the async is the Promise. The Angular does not wait for the ngOnDestroy‘s Promise, and the wait is the caller’s. The two are the pair, and the pair is the limitation.

Why the service’s ngOnDestroy should be the simple. The ngOnDestroy should be the simple, and the simple is the sync’s. The async’s cleanup is the DestroyRef‘s, and the DestroyRef is the modern. The two are the pair, and the pair is the pattern.


The DestroyRef

The DestroyRef is the injectable that signals the destruction. The onDestroy method registers the callback, and the callback runs when the injector is destroyed.

@Injectable({ providedIn: 'root' })
export class WebsocketService {
  private readonly destroyRef = inject(DestroyRef);
  private readonly socket = new WebSocket('wss://example.com');

  constructor() {
    this.destroyRef.onDestroy(() => {
      this.socket.close();
    });
  }
}

The destroyRef.onDestroy registers the callback, and the callback closes the socket. The DestroyRef is the injectable, and the onDestroy is the registration.

Why the DestroyRef matters. The DestroyRef is the modern, and the modern is the inject(). The OnDestroy interface is the classic, and the classic is the class’s. The two are the equivalent, and the DestroyRef is the modern.

Why the DestroyRef‘s onDestroy matters. The onDestroy is the callback’s registration, and the registration is the cleanup. The multiple callbacks can be registered, and the multiple are the sequence. The two are the pair, and the pair is the pattern.

Why the DestroyRef is the function’s. The DestroyRef can be used in the function, and the function is the inject()‘s. The class is not needed, and the function is the concise. The two are the pair, and the pair is the modern.

function createWebsocket(): WebSocket {
  const socket = new WebSocket('wss://example.com');
  inject(DestroyRef).onDestroy(() => socket.close());
  return socket;
}

The createWebsocket is the function, and the inject(DestroyRef) is the DestroyRef‘s. The function is the modern, and the modern is the concise. The two are the pair, and the pair is the pattern.

Why the DestroyRef‘s context matters. The inject(DestroyRef) requires the context, and the context is the construction’s. The function’s inject is the context’s, and the context is the requirement. The two are the pair, and the pair is the rule.

Why the DestroyRef should be the preferred. The DestroyRef is the preferred, and the preferred is the modern. The OnDestroy is the classic, and the classic is the alternative. The two are the choice, and the choice is the style.

Why the DestroyRef matters for the third-party. The DestroyRef is the third-party’s, and the third-party is the library’s. The library can use the DestroyRef without the class, and the class is the not needed. The two are the pair, and the pair is the library.


The takeUntilDestroyed operator

The takeUntilDestroyed operator completes the Observable when the injector is destroyed. The operator is the RxJS’s, and the completion is the cleanup.

@Component({ selector: 'app-user', standalone: true, template: `` })
export class UserComponent {
  private readonly destroyRef = inject(DestroyRef);

  constructor() {
    this.userService.user$.pipe(
      takeUntilDestroyed(this.destroyRef),
    ).subscribe((user) => {
      console.log(user);
    });
  }
}

The takeUntilDestroyed(this.destroyRef) completes the user$ when the component is destroyed, and the subscription is released. The operator is the cleanup, and the cleanup is the automatic.

Why the takeUntilDestroyed matters. The takeUntilDestroyed is the modern, and the modern is the concise. The Subject + the takeUntil is the classic, and the classic is the verbose. The two are the equivalent, and the takeUntilDestroyed is the modern.

Why the takeUntilDestroyed‘s context matters. The takeUntilDestroyed requires the context, and the context is the construction’s. The operator can be used without the argument if the context is the construction’s, and the argument is the DestroyRef‘s. The two are the pair, and the pair is the rule.

// With the context (the construction's):
this.user$.pipe(takeUntilDestroyed()).subscribe();

// With the explicit (the outside):
this.user$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();

The takeUntilDestroyed() is the context’s, and the takeUntilDestroyed(this.destroyRef) is the explicit’s. The two are the equivalent, and the two are the choice.

Why the takeUntilDestroyed should be the last. The takeUntilDestroyed should be the last, and the last is the pipeline’s. The operator completes the stream, and the completion is the cleanup. The two are the pair, and the pair is the order.

Why the takeUntilDestroyed matters for the leak. The takeUntilDestroyed prevents the leak, and the leak is the subscription’s. The subscription holds the reference, and the reference is the memory’s. The two are the pair, and the pair is the leak.

Why the takeUntilDestroyed is the modern. The takeUntilDestroyed is the modern, and the modern is the Angular 16+. The Subject + the takeUntil is the classic, and the classic is the legacy. The two are the equivalent, and the modern is the preference.


The stateful service’s scope

The stateful service’s scope determines the state’s lifetime. The root’s state is the application’s, and the component’s state is the component’s.

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<CartItem[]>([]);
  readonly cartItems = this.items.asReadonly();

  add(item: CartItem): void {
    this.items.update((list) => [...list, item]);
  }
}

The CartService is the root’s, and the state is the application’s. The cart’s items are the application’s, and the application’s is the shared.

Why the root’s state is the shared. The root’s state is the shared, and the shared is the application’s. The cart’s items are the application’s, and the application’s is the shared. The two are the pair, and the pair is the state.

Why the component’s state is the local. The component’s state is the local, and the local is the component’s. The component’s service is the component’s, and the component’s is the local. The two are the pair, and the pair is the scope.

@Component({
  selector: 'app-form',
  providers: [FormStateService],
})
export class FormComponent {}

The FormStateService is the component’s, and the state is the component’s. The form’s state is the component’s, and the component’s is the local. The two are the pair, and the pair is the scope.

Why the state’s scope matters. The state’s scope determines the state’s lifetime, and the lifetime determines the state’s persistence. The root’s state is the persistent, and the component’s is the transient. The two are the pair, and the pair is the design.

Why the wrong scope is the bug. The wrong scope is the bug, and the bug is the state’s. The component’s state that should be the root’s is the lost, and the root’s state that should be the component’s is the shared. The two are the pair, and the pair is the bug.

Why the state’s scope should be the documented. The state’s scope should be the documented, and the documented is the clarity. The scope is the design, and the design is the documented. The two are the pair, and the pair is the discipline.

Why the state’s scope matters for the test. The state’s scope matters for the test, and the test is the isolation’s. The component’s scope is the test’s, and the test’s is the isolation. The two are the pair, and the pair is the test.


The scoped cache

The scoped cache is the service that holds the data for its scope’s lifetime. The route’s cache is the route’s, and the component’s is the component’s.

@Injectable()
export class RouteCacheService {
  private readonly cache = new Map<string, unknown>();

  get<T>(key: string): T | undefined {
    return this.cache.get(key) as T | undefined;
  }

  set(key: string, value: unknown): void {
    this.cache.set(key, value);
  }

  clear(): void {
    this.cache.clear();
  }
}

const routes: Routes = [
  {
    path: 'products',
    providers: [RouteCacheService],
    children: [
      { path: '', component: ProductListComponent },
      { path: ':id', component: ProductDetailComponent },
    ],
  },
];

The RouteCacheService is the route’s, and the cache is the route’s. The ProductListComponent and the ProductDetailComponent share the cache, and the sharing is the route’s.

Why the route’s cache matters. The route’s cache is the feature’s, and the feature’s is the shared. The route’s components share the cache, and the sharing is the performance. The two are the pair, and the pair is the cache.

Why the component’s cache matters. The component’s cache is the component’s, and the component’s is the local. The component’s cache is the component’s state, and the state is the local. The two are the pair, and the pair is the component.

Why the cache’s cleanup matters. The cache’s cleanup is the ngOnDestroy‘s, and the ngOnDestroy is the service’s. The clear() is the cleanup, and the cleanup is the release. The two are the pair, and the pair is the memory.

Why the cache’s scope matters. The cache’s scope determines the cache’s lifetime, and the lifetime determines the cache’s validity. The route’s cache is the route’s, and the route’s is the feature’s. The two are the pair, and the pair is the scope.

Why the cache’s scope should be the deliberate. The cache’s scope should be the deliberate, and the deliberate is the design. The route’s cache is the route’s, and the component’s is the component’s. The two are the choice, and the choice is the design.

Why the cache’s scope matters for the memory. The cache’s scope matters for the memory, and the memory is the leak’s. The long-lived cache is the memory’s growth, and the growth is the leak. The two are the pair, and the pair is the memory.


The service’s factory scope

The service’s factory can create the scoped instance, and the factory’s scope is the injector’s.

export const SESSION_ID = new InjectionToken<string>('SESSION_ID', {
  providedIn: 'root',
  factory: () => crypto.randomUUID(),
});

The SESSION_ID‘s factory creates the crypto.randomUUID(), and the providedIn: 'root' is the root’s. The session’s ID is the application’s, and the application’s is the singleton.

Why the factory’s scope matters. The factory’s scope is the injector’s, and the injector is the provider’s. The providedIn: 'root' is the root’s, and the root’s is the singleton. The two are the pair, and the pair is the scope.

Why the factory’s scope can be the component’s. The factory’s scope can be the component’s, and the component’s is the per-instance. The factory creates the instance per the component, and the per-instance is the scope. The two are the pair, and the pair is the component’s.

@Component({
  selector: 'app-form',
  providers: [
    {
      provide: SESSION_ID,
      useFactory: () => crypto.randomUUID(),
    },
  ],
})
export class FormComponent {}

The SESSION_ID is the component’s, and the factory creates the instance per the component. The per-instance is the scope, and the scope is the component’s.

Why the factory’s scope’s choice matters. The factory’s scope’s choice matters, and the choice is the design. The root’s factory is the singleton, and the component’s is the per-instance. The two are the choice, and the choice is the design.

Why the factory’s scope should be the documented. The factory’s scope should be the documented, and the documented is the clarity. The scope is the design, and the design is the documented. The two are the pair, and the pair is the discipline.

Why the factory’s scope matters for the state. The factory’s scope matters for the state, and the state is the factory’s. The root’s factory is the singleton’s state, and the component’s is the local’s. The two are the pair, and the pair is the state.

Why the factory’s scope matters for the performance. The factory’s scope matters for the performance, and the performance is the instance’s. The root’s factory is the once, and the component’s is the per-component. The two are the pair, and the pair is the performance.


The scope’s pitfalls

The wrong scope is the bug, and the bug is the state’s. The common pitfalls are the root’s state that should be the component’s, the component’s state that should be the root’s, and the missing cleanup.

The pitfall 1: the root’s state that should be the component’s. The component’s state is stored in the root’s service, and the state is shared across the components. The state is the wrong scope, and the scope is the bug.

// Wrong: the root's service holds the form's state
@Injectable({ providedIn: 'root' })
export class FormStateService {
  readonly formData = signal<FormData | null>(null);
}

The FormStateService is the root’s, and the form’s state is the application’s. The two forms share the state, and the sharing is the bug. The fix is the component’s scope.

Why the root’s state is the bug. The root’s state is the shared, and the shared is the application’s. The form’s state should be the component’s, and the component’s is the local. The two are the pair, and the pair is the scope.

The pitfall 2: the component’s state that should be the root’s. The application’s state is stored in the component’s service, and the state is lost when the component is destroyed. The state is the wrong scope, and the scope is the bug.

// Wrong: the component's service holds the application's state
@Component({
  selector: 'app-shell',
  providers: [AuthService],
})
export class ShellComponent {}

The AuthService is the component’s, and the auth’s state is the component’s. The auth’s state should be the application’s, and the application’s is the root’s. The two are the pair, and the pair is the scope.

Why the component’s state is the bug. The component’s state is the local, and the local is the transient. The auth’s state should be the application’s, and the application’s is the persistent. The two are the pair, and the pair is the scope.

The pitfall 3: the missing cleanup. The service’s ngOnDestroy is missing, and the resource is the leak. The cleanup is the requirement, and the requirement is the discipline.

// Wrong: the missing ngOnDestroy
@Injectable({ providedIn: 'root' })
export class WebsocketService {
  private readonly socket = new WebSocket('wss://example.com');
  // The ngOnDestroy is missing, and the socket is the leak.
}

The WebsocketService‘s ngOnDestroy is missing, and the socket is the leak. The cleanup is the requirement, and the requirement is the discipline. The two are the pair, and the pair is the cleanup.

Why the missing cleanup is the bug. The missing cleanup is the leak, and the leak is the memory’s. The resource is the socket, the timer, the subscription, and the resource needs the release. The two are the pair, and the pair is the cleanup.

The pitfall 4: the wrong lifetime’s assumption. The component’s service’s lifetime is the component’s, and the state is the component’s. The assumption of the root’s lifetime is the wrong, and the wrong is the bug.

Why the lifetime’s assumption matters. The lifetime’s assumption is the design’s, and the design’s is the scope. The component’s lifetime is the component’s, and the component’s is the transient. The two are the pair, and the pair is the scope.

Why the pitfalls are the common. The pitfalls are the common, and the common is the scope’s. The scope’s choice is the design’s, and the design’s is the state’s. The two are the pair, and the pair is the discipline.


Complete Example Session

import {
  Injectable, Component, Directive, inject, signal, DestroyRef,
  OnDestroy, InjectionToken,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

// ============================================
// PART 1: THE ROOT SERVICE
// ============================================

@Injectable({ providedIn: 'root' })
export class UserService implements OnDestroy {
  private readonly users = signal<User[]>([]);

  ngOnDestroy(): void {
    console.log('UserService destroyed');
  }
}

// ============================================
// PART 2: THE ROUTE SERVICE
// ============================================

@Injectable()
export class AdminService implements OnDestroy {
  private readonly cache = new Map<string, unknown>();

  ngOnDestroy(): void {
    this.cache.clear();
  }
}

const routes: Routes = [
  {
    path: 'admin',
    providers: [AdminService],
    children: [{ path: '', component: AdminComponent }],
  },
];

// ============================================
// PART 3: THE COMPONENT SERVICE
// ============================================

@Injectable()
export class FormStateService {
  readonly formData = signal<FormData | null>(null);
}

@Component({
  selector: 'app-form',
  providers: [FormStateService],
})
export class FormComponent {}

// ============================================
// PART 4: THE DESTROY REF
// ============================================

@Injectable({ providedIn: 'root' })
export class WebsocketService {
  private readonly destroyRef = inject(DestroyRef);
  private readonly socket = new WebSocket('wss://example.com');

  constructor() {
    this.destroyRef.onDestroy(() => {
      this.socket.close();
    });
  }
}

// ============================================
// PART 5: THE TAKE UNTIL DESTROYED
// ============================================

@Component({ selector: 'app-user', standalone: true, template: `` })
export class UserComponent {
  private readonly userService = inject(UserService);

  constructor() {
    this.userService.user$.pipe(
      takeUntilDestroyed(),
    ).subscribe((user) => {
      console.log(user);
    });
  }
}

// ============================================
// PART 6: THE SCOPED CACHE
// ============================================

@Injectable()
export class RouteCacheService implements OnDestroy {
  private readonly cache = new Map<string, unknown>();

  get<T>(key: string): T | undefined {
    return this.cache.get(key) as T | undefined;
  }

  set(key: string, value: unknown): void {
    this.cache.set(key, value);
  }

  ngOnDestroy(): void {
    this.cache.clear();
  }
}

const cachedRoutes: Routes = [
  {
    path: 'products',
    providers: [RouteCacheService],
    children: [
      { path: '', component: ProductListComponent },
      { path: ':id', component: ProductDetailComponent },
    ],
  },
];

// ============================================
// PART 7: THE SESSION'S ID
// ============================================

export const SESSION_ID = new InjectionToken<string>('SESSION_ID', {
  providedIn: 'root',
  factory: () => crypto.randomUUID(),
});

// ============================================
// PART 8: THE COMPONENT'S SESSION
// ============================================

@Component({
  selector: 'app-session',
  providers: [
    { provide: SESSION_ID, useFactory: () => crypto.randomUUID() },
  ],
})
export class SessionComponent {}

// ============================================
// PART 9: THE STATE'S SCOPE
// ============================================

// The root's state: the application's
@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<CartItem[]>([]);
  readonly cartItems = this.items.asReadonly();
}

// The component's state: the component's
@Injectable()
export class FormStateService {
  readonly formData = signal<FormData | null>(null);
}

@Component({ providers: [FormStateService] })
export class FormComponent {}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use the root's service for the component's state
// The root's state is the shared.                              // ⚠️

// Don't use the component's service for the application's state
// The component's state is the lost.                           // ⚠️

// Don't forget the ngOnDestroy
// The missing cleanup is the leak.                             // ⚠️

// Don't forget the takeUntilDestroyed
// The missing operator is the leak.                            // ⚠️

// Don't assume the wrong lifetime
// The scope determines the lifetime.                           // ⚠️

// Don't forget the scoped cache's cleanup
// The cache's growth is the memory's.                          // ⚠️

The ten parts cover the root’s service, the route’s service, the component’s service, the DestroyRef, the takeUntilDestroyed, the scoped cache, the session’s ID, the component’s session, the state’s scope, and the anti-patterns.


Quick Reference

The Scopes

The scopeThe lifetimeThe declaration
The rootThe applicationThe providedIn: 'root'
The routeThe routeThe route’s providers
The componentThe componentThe @Component({ providers })
The directiveThe directiveThe @Directive({ providers })
The elementThe elementThe element’s providers

The Lifecycle’s Hooks

The hookThe scope
The ngOnDestroy (the service)The injector’s destruction
The ngOnDestroy (the component)The component’s destruction
The DestroyRef.onDestroyThe injector’s destruction

The Cleanup’s Tools

The toolThe purpose
The DestroyRefThe destruction’s signal
The takeUntilDestroyedThe Observable’s completion
The ngOnDestroyThe class’s cleanup

The State’s Scope

The stateThe scope
The application’sThe root’s
The feature’sThe route’s
The localThe component’s

The takeUntilDestroyed‘s Forms

The formThe context
The takeUntilDestroyed()The construction’s
The takeUntilDestroyed(this.destroyRef)The outside’s

The Scoped Cache

The cacheThe scope
The application’sThe root’s
The feature’sThe route’s
The component’sThe component’s

Best Practices

✅ Do This:

// Use the providedIn: 'root' for the application's state
@Injectable({ providedIn: 'root' })
export class CartService {}                                    // ✅

// Use the route's providers for the feature's state
{ path: 'admin', providers: [AdminService] }                   // ✅

// Use the component's providers for the local state
@Component({ providers: [FormStateService] })
export class FormComponent {}                                  // ✅

// Use the ngOnDestroy for the cleanup
@Injectable()
export class WebsocketService implements OnDestroy {
  ngOnDestroy(): void { this.socket.close(); }
}                                                              // ✅

// Use the DestroyRef for the modern
this.destroyRef.onDestroy(() => this.socket.close());          // ✅

// Use the takeUntilDestroyed for the Observable
this.user$.pipe(takeUntilDestroyed()).subscribe();             // ✅

// Clear the cache in the ngOnDestroy
ngOnDestroy(): void { this.cache.clear(); }                    // ✅

❌ Don’t Do This:

// Don't use the root's service for the component's state
@Injectable({ providedIn: 'root' })
export class FormStateService {}  // the shared's bug             // ⚠️

// Don't use the component's service for the application's state
@Component({ providers: [AuthService] })
export class ShellComponent {}  // the lost's bug                 // ⚠️

// Don't forget the ngOnDestroy
@Injectable()
export class WebsocketService {}  // the leak's bug               // ⚠️

// Don't forget the takeUntilDestroyed
this.user$.subscribe();  // the leak's bug                       // ⚠️

// Don't assume the wrong lifetime
// The scope determines the lifetime.                          // ⚠️

// Don't forget the scoped cache's cleanup
// The cache's growth is the memory's.                         // ⚠️

Common Pitfalls

PitfallProblemSolution
The root’s state for the component’sThe sharedThe component’s scope
The component’s state for the application’sThe lostThe root’s scope
The missing ngOnDestroyThe leakThe cleanup
The missing takeUntilDestroyedThe leakThe operator
The wrong lifetime’s assumptionThe bugThe scope’s check
The scoped cache’s growthThe memoryThe clear()
The service’s async’s cleanupThe incompleteThe DestroyRef
The directive’s scopeThe element’sThe directive’s

Real-World Examples

1. The root’s service

@Injectable({ providedIn: 'root' })
export class UserService {}

2. The route’s service

{ path: 'admin', providers: [AdminService], children: [...] }

3. The component’s service

@Component({ providers: [FormStateService] })
export class FormComponent {}

4. The ngOnDestroy

@Injectable()
export class WebsocketService implements OnDestroy {
  ngOnDestroy(): void { this.socket.close(); }
}

5. The DestroyRef

this.destroyRef.onDestroy(() => this.socket.close());

6. The takeUntilDestroyed

this.user$.pipe(takeUntilDestroyed()).subscribe();

7. The scoped cache

@Injectable()
export class RouteCacheService implements OnDestroy {
  ngOnDestroy(): void { this.cache.clear(); }
}

8. The session’s ID

new InjectionToken<string>('SESSION_ID', {
  providedIn: 'root',
  factory: () => crypto.randomUUID(),
});

9. The component’s session

@Component({
  providers: [{ provide: SESSION_ID, useFactory: () => crypto.randomUUID() }],
})

10. The state’s scope

@Injectable({ providedIn: 'root' })
export class CartService {}  // the application's

Visual: The Scopes and the Lifetimes

┌──────────────────────────────────────────────────────────┐
│  THE ROOT'S SCOPE                                        │
│    The application's lifetime.                           │
│    The providedIn: 'root'.                               │
│    The shared's state.                                   │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE ROUTE'S SCOPE                                       │
│    The route's lifetime.                                 │
│    The route's providers.                                │
│    The feature's state.                                  │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE COMPONENT'S SCOPE                                   │
│    The component's lifetime.                             │
│    The @Component({ providers }).                        │
│    The local's state.                                    │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE LIFETIME'S ORDER                                    │
│    The component's create → the route's load → the       │
│    application's start.                                  │
│                                                          │
│  The destroy is the reverse.                             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The DestroyRef and the takeUntilDestroyed

┌──────────────────────────────────────────────────────────┐
│  THE DestroyRef                                          │
│    const destroyRef = inject(DestroyRef);                │
│    destroyRef.onDestroy(() => { this.socket.close(); }); │
│                                                          │
│  THE takeUntilDestroyed                                  │
│    this.user$.pipe(takeUntilDestroyed()).subscribe();    │
│                                                          │
│  THE DESTROY'S SEQUENCE                                  │
│    1. The injector is destroyed.                         │
│    2. The DestroyRef's callbacks run.                    │
│    3. The takeUntilDestroyed's streams complete.         │
│    4. The ngOnDestroy's methods run.                     │
│                                                          │
│  The cleanup is the automatic.                           │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The State’s Scope

┌──────────────────────────────────────────────────────────┐
│  THE ROOT'S STATE                                        │
│    The application's, the shared.                        │
│    The user's session, the cart, the theme.              │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE ROUTE'S STATE                                       │
│    The feature's, the route's lifetime.                  │
│    The admin's cache, the product's filter.              │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  THE COMPONENT'S STATE                                   │
│    The local's, the component's lifetime.                │
│    The form's data, the dialog's open.                   │
│                                                          │
│  THE WRONG SCOPE'S BUGS                                  │
│    The root's state for the form → the shared's bug.     │
│    The component's state for the auth → the lost's bug.  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Scoped Cache

┌──────────────────────────────────────────────────────────┐
│  THE ROUTE'S CACHE                                       │
│    The route's providers.                                │
│    The ProductListComponent and the ProductDetailComponent│
│    share the cache.                                      │
│                                                          │
│  THE CLEANUP                                             │
│    The ngOnDestroy clears the cache.                     │
│    The route's unload releases the memory.               │
│                                                          │
│  THE CACHE'S LIFETIME                                    │
│    The route's load → the cache's create.                │
│    The route's unload → the cache's destroy.             │
│                                                          │
│  The scope determines the lifetime, and the lifetime     │
│  determines the cache's validity.                        │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Service’s ngOnDestroy

┌──────────────────────────────────────────────────────────┐
│  @Injectable({ providedIn: 'root' })                     │
│  export class WebsocketService implements OnDestroy {    │
│    private readonly socket = new WebSocket('wss://...'); │
│                                                          │
│    ngOnDestroy(): void {                                 │
│      this.socket.close();                                │
│    }                                                     │
│  }                                                       │
│                                                          │
│  THE TIMING                                              │
│    The service's creation → the first inject.            │
│    The service's ngOnDestroy → the injector's destroy.   │
│                                                          │
│  THE ROOT'S INJECTOR                                     │
│    The application's destroy → the ngOnDestroy.          │
│                                                          │
│  The scope determines the lifetime.                      │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
The root’s scopeThe application’s lifetime
The route’s scopeThe route’s lifetime
The component’s scopeThe component’s lifetime
The directive’s scopeThe directive’s lifetime
The ngOnDestroyThe cleanup
The DestroyRefThe destruction’s signal
The takeUntilDestroyedThe Observable’s completion
The takeUntilDestroyed()The construction’s context
The state’s scopeThe root, the route, the component
The scoped cacheThe route’s, the component’s

Key takeaways:

  • The scope determines the lifetime, and the lifetime determines the state — the root’s scope is the application’s, the route’s is the feature’s, and the component’s is the local
  • The root’s providedIn: 'root' is the application’s singleton — the service is created on the first inject, and destroyed with the application
  • The route’s providers is the route’s lifetime — the service is created when the route loads, and destroyed when the route unloads
  • The component’s providers is the component’s lifetime — the service is created with the component, and destroyed with the component
  • The ngOnDestroy is the cleanup — the service’s ngOnDestroy runs when the injector is destroyed, and the component’s runs when the component is destroyed
  • The DestroyRef is the modern cleanup’s signal — the onDestroy registers the callback, and the callback runs on the destruction
  • The takeUntilDestroyed completes the Observable — the operator is the RxJS’s, and the completion is the cleanup
  • The state’s scope is the design decision — the wrong scope is the bug, and the bug is the state’s
  • The scoped cache’s lifetime is the scope’s — the route’s cache is the route’s, and the component’s is the component’s
  • The missing cleanup is the leak — the ngOnDestroy, the DestroyRef, and the takeUntilDestroyed are the prevention

Remember: The scope determines the lifetime, and the lifetime determines the state. The root’s scope is the application’s, the route’s is the feature’s, and the component’s is the local. The ngOnDestroy, the DestroyRef, and the takeUntilDestroyed are the cleanup’s tools, and the cleanup is the discipline. The wrong scope is the bug, and the bug is the state’s. The scope is the design, and the design is the application’s.


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!