Inyección de Dependencias en Angular: Dominando Servicios y Providers
La Inyección de Dependencias (DI) es el corazón de Angular. Es el mecanismo que permite a nuestras clases recibir las herramientas que necesitan para funcionar sin tener que crearlas internamente. En este artículo vamos a desglosar cada pieza del sistema, desde lo más básico hasta patrones avanzados con ejemplos reales.
¿Qué es la Inyección de Dependencias?
En términos simples, la DI es un patrón de diseño donde una clase solicita dependencias externas en lugar de instanciarlas ella misma. En Angular, esto se logra a través del Injector, que actúa como un contenedor que sabe cómo crear y entregar estas piezas.
Imagina un restaurante: el chef (tu componente) no va al mercado a comprar los ingredientes. En su lugar, el restaurante (el Injector) se encarga de proveer todo lo necesario para que el chef se concentre en cocinar.
¿Por qué es tan importante?
- Testabilidad: Puedes reemplazar servicios reales por mocks en tus tests.
- Desacoplamiento: Tus componentes no conocen los detalles de implementación de los servicios.
- Reutilización: Un mismo servicio puede servir a múltiples componentes sin duplicar código.
¿Cuándo deberías usar un Servicio?
No todo el código debe ir en los componentes. Debes extraer lógica a un servicio cuando:
- Compartir datos: Cuando varios componentes necesitan acceder a la misma información.
- Lógica de Negocio: Para mantener tus componentes 'limpios' y enfocados solo en la visualización.
- Llamadas API: Toda la interacción con el
HttpClientdebe vivir en servicios. - Encapsulamiento: Cuando quieres ocultar la complejidad de una implementación tras una interfaz sencilla.
Ejemplo práctico: ¿Componente o Servicio?
// ❌ MAL: Lógica de negocio en el componente
@Component({ ... })
export class ProductListComponent {
products: Product[] = [];
async ngOnInit() {
const response = await fetch('/api/products');
const data = await response.json();
this.products = data.filter(p => p.active).sort((a, b) => a.price - b.price);
}
}
// ✅ BIEN: Lógica delegada al servicio
@Component({ ... })
export class ProductListComponent {
private productService = inject(ProductService);
products = toSignal(this.productService.getActiveProducts());
}
Creación y Configuración de un Servicio
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root' // Esto lo convierte en un Singleton disponible en toda la app
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl = '/api/products';
getActiveProducts() {
return this.http.get<Product[]>(this.apiUrl).pipe(
map(products => products.filter(p => p.active)),
map(products => products.sort((a, b) => a.price - b.price))
);
}
getById(id: string) {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
}
El decorador @Injectable({ providedIn: 'root' }) indica que Angular debe crear una única instancia (Singleton) disponible en toda la aplicación. Esto es lo más común y lo recomendado por el equipo de Angular.
Formas de Inyectar Dependencias
A partir de las versiones modernas de Angular, tenemos dos formas principales de inyectar servicios:
1. Vía Constructor (Tradicional)
@Component({ ... })
export class UserComponent {
constructor(private userService: UserService) {}
}
2. Función inject() (Moderna y recomendada)
La función inject() es la forma recomendada en Angular moderno, especialmente útil en funciones standalone y composición de lógica.
import { inject } from '@angular/core';
@Component({ ... })
export class UserComponent {
private userService = inject(UserService);
private router = inject(Router);
private activatedRoute = inject(ActivatedRoute);
}
¿Cuándo usar cada una?
| Característica | Constructor | inject() |
|---|---|---|
| Angular moderno | ✅ | ✅ (preferida) |
| Funciones standalone | ❌ | ✅ |
| Guards funcionales | ❌ | ✅ |
| Interceptors funcionales | ❌ | ✅ |
| Herencia de clases | Más verboso | Más limpio |
El Árbol de Inyectores (Hierarchical Injection)
Una de las potencias de Angular es que los inyectores son jerárquicos:
- Root Injector: El servicio es una instancia única para toda la aplicación.
- Component Injector: Si declaras un servicio en el array
providers: []de un componente, se creará una instancia única para ese componente y sus hijos. Al destruir el componente, el servicio también se destruye.
Ejemplo: Servicio con scope limitado
// Este servicio tendrá una instancia POR CADA TabComponent
@Component({
selector: 'app-tab',
providers: [TabStateService], // Instancia única por componente
template: `<div>{{ state.title() }}</div>`
})
export class TabComponent {
state = inject(TabStateService);
}
InjectionToken: Inyectar valores que no son clases
No siempre inyectamos servicios. A veces necesitamos inyectar valores de configuración, strings o funciones:
import { InjectionToken } from '@angular/core';
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const IS_PRODUCTION = new InjectionToken<boolean>('IS_PRODUCTION');
// En el bootstrap o providers
providers: [
{ provide: API_BASE_URL, useValue: 'https://api.ejemplo.com' },
{ provide: IS_PRODUCTION, useValue: environment.production },
]
// En un servicio
@Injectable({ providedIn: 'root' })
export class ApiService {
private baseUrl = inject(API_BASE_URL);
}
Buenas Prácticas
- Principio de Responsabilidad Única: Un servicio debe hacer una sola cosa (ej.
AuthServicesolo para autenticación). - Evitar lógica en el Constructor: Usa el constructor o el
inject()solo para asignar dependencias. La lógica de inicialización debe ir en el hookngOnInit. - Interfaces: Siempre que sea posible, utiliza tipos claros para lo que devuelven tus servicios.
- Prefiere
providedIn: 'root': Evita registrar servicios manualmente enproviderssalvo que necesites scope limitado.
Conclusión
Dominar la Inyección de Dependencias te permite escribir código más testeable, modular y fácil de mantener. Al mover la lógica pesada a los servicios, tus componentes se vuelven piezas ligeras dedicadas exclusivamente a la experiencia de usuario.