Understanding Service Instances and Dependency Injection in Angular: Why Root-Level Services Aren’t Always Singletons

Gili Yaniv
4 min readNov 3, 2024

--

When working with Angular services, one might expect that services provided at the root level, using the `providedIn: ‘root’` option, should behave as singletons. In most cases, this is true, but there are certain scenarios where the behavior might seem counterintuitive. One such situation arises when a service is injected multiple times using the providers array, leading to the creation of a new instance of that service. This article will explore why the value of state.name$ inside the `AppChild` component remains `’Initial value’` instead of reflecting `’Hello world’` and why injecting a root-level service sometimes creates multiple instances.

Scenario Setup

Let’s break down the provided code example:

  1. StateService:
@Injectable({
providedIn: 'root',
})
export class StateService {
nameSubject = new BehaviorSubject('Initial value');
name$ = this.nameSubject.asObservable();
}

This service manages a `BehaviorSubject`, which starts with an initial value of `’Initial value’`. It exposes the observable `name$` to which other components can subscribe.

2. AppChild Component:

@Component({
selector: 'app-child',
standalone: true,
providers: [StateService], // Injects a new instance of StateService
template: `<p> State name is - {{state.name$ | async}}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppChild {
state = inject(StateService);
}

This component uses a local `StateService` instance (defined in the `providers` array), which is distinct from the one provided at the root level.

3. App Component:

@Component({
selector: 'app-root',
standalone: true,
imports: [AppChild],
template: `<app-child></app-child>`,
})
export class App {
state = inject(StateService);
ngOnInit() {
// Changes the root-level StateService instance's value
this.state.nameSubject.next('Hello world');
}
}

This component modifies the `StateService`’s value, but it is important to note that this refers to the root-level service instance.

Why `AppChild` Shows `’Initial value’` Instead of `’Hello world’`

At first glance, it might seem counterintuitive that the `state.name$` value inside the `AppChild` component displays `’Initial value’`, even though the `App` component changes the `StateService`’s `nameSubject` to `’Hello world’`. Here’s why this happens:

1. Local vs. Global Service Instances:
Even though `StateService` is provided at the root level (via `providedIn: ‘root’`), the `AppChild` component creates a new instance of `StateService` because it lists the service in its `providers` array. This means `AppChild` uses a completely different instance of the service, with its own `BehaviorSubject` that still holds the `’Initial value’`.

2. Separate State Management:
The root-level `StateService` instance (used by the `App` component) and the local `StateService` instance (used by the `AppChild` component) are not connected. As a result, when `App` changes the state to `’Hello world’`, it is updating the root-level instance’s `nameSubject`, but the `AppChild` component is still subscribed to its own local instance of the service, which continues to have `’Initial value’`.

3. The Key to Understanding:
- The `StateService` inside `App` is the root-level service instance, and the change to `’Hello world’` affects only that instance.
- The `StateService` inside `AppChild` is a separate instance because of the `providers: [StateService]` declaration, which overrides the root-level service with a new instance.

Why This Behavior Might Be Counterintuitive

Angular’s dependency injection system is designed to create singletons for services provided at the root level. When you specify `providedIn: ‘root’`, you expect the service to be shared across the entire application. However, when you explicitly provide the service again using the `providers` array in a component, Angular treats that as a request for a **new instance** of the service, even though it is provided in the root.

This behavior can be confusing because developers often assume that root-level services are singletons across the app. While this is generally true, the presence of a `providers` array in a component can override this, leading to multiple instances of the same service in different parts of the application.

Why Injecting a Root-Level Service Can Create Multiple Instances

The key takeaway is that root-level services are singletons unless explicitly overridden. If a component or module includes the same service in its `providers` array, Angular will treat it as a request for a new instance of that service.

- Root-Level Service Behavior:
If you inject a service provided in the root using `providedIn: ‘root’`, it behaves as a singleton throughout the application unless you provide it again locally in a component or module.

- Local Service Behavior:
When you declare a service in the `providers` array of a component, Angular creates a new instance of that service for that specific component and its children. This local instance is entirely separate from the root-level instance.

This allows for a lot of flexibility but can also lead to confusion, as it’s easy to assume that root-level services should always be singleton and shared across the entire app.

Conclusion

To sum up, the `state.name$` value inside `AppChild` shows `’Initial value’` instead of `’Hello world’` because `AppChild` is using a new instance of `StateService` due to the `providers` array declaration. Even though `StateService` is provided in the root, each component can request a new instance by adding the service to its own `providers` array.

This can be counterintuitive for developers who expect root-level services to always behave as singletons, but understanding that Angular creates new instances when explicitly requested through the `providers` array helps clarify this behavior. To avoid such issues, it’s essential to be mindful of where and how services are provided to ensure proper state sharing across your application.

Follow me on Twitter, Medium, and Linkedin to read more!

--

--

No responses yet