One Instance to Rule Them All: Mastering Angular Singletons
Dependency Injection (DI) is at the core of Angular. Unlike other frameworks where you might manually instantiate classes (e.g., `new UserService()`), Angular manages these instances for you. The most common and powerful pattern managed by this system is the **Singleton Service**.
What is a Singleton in Angular?
A Singleton service is a service for which a single instance exists in an injector. In Angular apps, there are two main injector hierarchies: the **ModuleInjector** hierarchy and the **ElementInjector** hierarchy.
When we say a service is a "singleton in the app", we usually mean it is provided in the application's **root injector**. This ensures that every component, directive, or pipe that requests this service receives the exact same object reference. This is critical for:
- **State Management:** Holding user authentication data, shopping cart contents, or UI themes.
- **Data Caching:** Storing API responses to avoid redundant network requests.
- **Communication:** Acting as a bus for cross-component communication via Observables/Subjects.
The Evolution: From `providers: []` to `providedIn: 'root'`
Before Angular 6, services were typically registered in the `providers` array of an `NgModule` (usually `AppModule`). While this worked, it had two drawbacks: it was verbose, and it made **tree-shaking** difficult.
✔️ Modern Approach
@Injectable({
providedIn: 'root'
})
export class MyService {}Enables tree-shaking. If the service isn't used, it's removed from the bundle.
❌ Legacy Approach
@NgModule({
providers: [MyService]
})
export class AppModule {}Service is included in the bundle even if unused.
The "Lazy Loading" Trap
The singleton nature holds true for the root injector. However, if you provide a service in the `providers` array of a **lazy-loaded module**, Angular creates a *child injector* for that module. This results in a **new instance** of the service specifically for that module, separate from the root instance. This is a common source of bugs where data updates in one part of the app aren't reflected in the lazy-loaded section.
Pro Tip: Always default to `providedIn: 'root'`. Only use `providers: []` in modules or components if you explicitly *need* multiple instances or need to override an existing provider for a specific scope.