Cómo Angular resuelve los servicios mediante la inyección de dependencias con inject()
Comprenda cómo Angular DI suministra servicios a través de inject(), providers e inyectores jerárquicos, y cómo delimitar instancias y evitar errores por proveedor faltante.
Una vez que comienzas a trasladar la lógica de los componentes Angular a servicios, surge una pregunta práctica: cuando un componente escribe inject(UserService), ¿de dónde proviene realmente ese objeto, quién lo construyó y qué instancia recibe el componente? La respuesta es el sistema de inyección de dependencias (DI) de Angular. Esta guía explica qué es el DI, cómo funcionan inject() y los proveedores, cómo los inyectores jerárquicos deciden qué instancia se obtendrá y cómo evitar el error más común entre principiantes, para que puedas compartir o delimitar servicios en tus propias aplicaciones con confianza.
Aquí está la línea que suele plantear la pregunta:
userService = inject(UserService);
Nada en el componente crea UserService, sin embargo, el campo termina conteniendo un objeto listo para usar. Fue proporcionado por el sistema de DI de Angular.
Por qué los componentes no deben construir sus propios servicios
La alternativa obvia es construir el servicio uno mismo dentro del componente:
userService = new UserService();
Funciona, pero vincula de forma rígida el componente a una clase concreta y a un único modo de construirlo. Ahora el componente decide cómo se crea el servicio, qué necesita para existir y si se debe crear una copia nueva cada vez. Si más adelante el servicio necesita sus propias dependencias, todo lugar que llame a new también tendrá que modificarse.
DI invierte esa responsabilidad. El componente solo declara que necesita UserService. Angular localiza un proveedor correspondiente, crea la instancia o reutiliza una ya existente y se la entrega:
Component
│
│ "I need UserService"
↓
Angular DI System
│
↓
UserService
Las dos palabras del nombre describen exactamente esto. Una dependencia es cualquier cosa que una clase necesita para realizar su trabajo. Inyección significa que el framework entrega esa dependencia a la clase en lugar de que la clase la obtenga o la construya.
Primera inyección, de punta a punta
Comience con un servicio pequeño. El decorador @Injectable marca la clase como algo que el DI de Angular puede crear, y providedIn: 'root' la registra en toda la aplicación:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
El componente solicita el servicio mediante inject(), importado desde @angular/core junto con Component:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
}
Cuando Angular construye App, evalúa esta expresión:
inject(UserService)
Angular lo interpreta como “esta clase necesita un UserService”, resuelve la dependencia y asigna el resultado al campo. Dado que el campo es público, el template puede llamar directamente al servicio:
<h1>{{ userService.getUserName() }}</h1>
Observe lo que falta en el componente. No hay ninguna llamada de construcción en ningún lugar:
new UserService()
Crear y entregar la instancia es exclusivamente responsabilidad de Angular.
Trabajar con inject()
inject() es la API moderna para solicitar una dependencia del sistema DI de Angular. Se le pasa un token, generalmente la propia clase del servicio, y se recibe la instancia:
userService = inject(UserService);
La función proviene del paquete principal de Angular:
import { Component, inject } from '@angular/core';
Con el campo asignado en el cuerpo de la clase:
userService = inject(UserService);
cada método de la clase puede acceder al servicio a través de this:
this.userService
Por ejemplo, un método que lee el nombre y lo registra:
loadUser() {
const name = this.userService.getUserName();
console.log(name);
}
Una regla a recordar: inject() solo funciona dentro de un contexto de inyección. En la práctica, eso significa inicializadores de campos, el constructor y algunos lugares gestionados por el framework como funciones de fábrica y guardias funcionales. Llamarlo más tarde, por ejemplo dentro de un manejador de clic o en una función de callback de setTimeout, genera un error. Obtenga la dependencia en un campo y luego utilícelo donde sea necesario.
inject() en comparación con la inyección por constructor
Los tutoriales antiguos y muchos conjuntos de código existentes solicitan dependencias a través de parámetros del constructor:
constructor(private userService: UserService) {}
Este estilo basado en el constructor sigue teniendo soporte total. El equivalente basado en campos se ve así:
userService = inject(UserService);
Ambos formularios se comunican con el mismo sistema DI y producen el mismo resultado. Para código nuevo, inject() tiende a ser más breve, encaja naturalmente con componentes independientes y otras API modernas de Angular, y evita largas listas de parámetros en los constructores, lo cual se vuelve evidente al usar herencia. Una clase típica termina viéndose así:
export class App {
userService = inject(UserService);
}
No obstante, la sintaxis es la parte menos importante. Lo que realmente importa es el principio: el componente recibe su dependencia de Angular en lugar de crearla.
Proveedores: indicarle a Angular cómo suministrar una dependencia
Solicitar una dependencia solo funciona si Angular sabe cómo generarla. Ese conocimiento se encuentra en un proveedor. La forma más común de declarar uno es mediante el decorador @Injectable:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
La opción clave está en esta única línea:
providedIn: 'root'
Registra UserService en el inyector raíz de la aplicación. A partir de entonces, se puede satisfacer cualquier solicitud como la siguiente:
inject(UserService)
Una forma útil de imaginar un proveedor es como una receta que Angular guarda en archivo:
"When someone asks for UserService,
here is how you should provide it."
Registro en el nivel raíz con providedIn
Puede que ya hayas visto esta configuración al crear servicios:
@Injectable({
providedIn: 'root'
})
Con providedIn: 'root', el servicio pertenece al inyector de nivel raíz. Angular crea una instancia la primera vez que alguien lo solicita y reutiliza esa misma instancia para todas las solicitudes posteriores, lo que lo convierte en un singleton a nivel de aplicación. El servicio completo se ve así:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Cualquier componente puede solicitarlo sin tener que incluirlo en el array providers:
userService = inject(UserService);
Para la mayoría de los servicios a nivel de aplicación, este es el enfoque más simple y preferido. También tiene un efecto secundario útil: si nadie inyecta el servicio, las herramientas de compilación pueden excluirlo del paquete, ya que el registro se encuentra en el servicio y no en un módulo o componente.
Registro de un servicio en un componente
También se puede asociar un proveedor a un componente específico a través de su array providers:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
En este caso, UserService es proporcionado por el propio inyector del componente y no solo por el inyector raíz:
providedIn: 'root'
La diferencia entre estos dos registros es el punto de partida para comprender los inyectores jerárquicos.
Inyectores jerárquicos
Angular no utiliza un único contenedor global. Los inyectores forman un árbol que refleja aproximadamente el árbol de componentes, con el inyector raíz en la parte superior:
Root Injector
│
├── App
│
├── Component A
│
│ └── Child Component
│
└── Component B
Un servicio proporcionado en la raíz está disponible casi en todas partes. Sin embargo, un componente puede registrar su propio proveedor para el mismo servicio:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
Ahora Angular mantiene un proveedor vinculado al inyector de ese componente. Cuando el componente o cualquiera de sus descendientes solicita UserService, Angular utiliza la instancia de ese nivel. Cada instancia del componente obtiene su propia copia del servicio, creada junto con el componente y destruida al mismo tiempo. Los componentes en otras partes del árbol siguen viendo la instancia raíz.
Por qué son útiles los múltiples niveles
Considere un SettingsService. La mayoría de las pantallas deberían compartir el mismo conjunto de configuraciones, por lo que es lógico proporcionarlo en la raíz:
Root
│
├── Home
├── Products
└── Profile
Supongamos ahora que el área de administración necesita sus propios ajustes que no deben filtrarse al resto de la aplicación:
Root
│
├── Home
│
└── Admin
│
└── Admin Settings Service
Al proporcionar el servicio en el componente de administración, ese subárbol recibe una instancia separada mientras todo lo demás sigue utilizando la compartida. El estado por funcionalidad, el estado por diálogo y el estado por elemento de lista son usos comunes de este patrón, y ninguno de ellos es posible cuando cada componente simplemente llama a new.
Cómo resuelve Angular una solicitud
Cuando Angular se encuentra con una solicitud como esta:
userService = inject(UserService);
busca un proveedor comenzando por el inyector más cercano al solicitante y avanzando hacia afuera:
Component requests UserService
↓
Check current injector
↓
Provider found?
↙ ↘
Yes No
↓ ↓
Provide it Look at parent
↓
Continue upward
El primer inyector que encuentre un proveedor compatible gana. Si la búsqueda llega al principio sin encontrar ninguno, Angular lanza un error de DI, que generalmente se muestra como NullInjectorError: No provider for UserService. Por eso, cada servicio inyectable debe estar registrado en un lugar al que pueda acceder quien lo solicite.
Inyección de varias dependencias
Un componente puede depender de tantos servicios como necesite. Aquí un componente solicita tanto un servicio de usuario como uno de notificaciones:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
notificationService = inject(NotificationService);
}
Luego, un método puede combinarlos, por ejemplo, para saludar al usuario actual:
showWelcomeMessage() {
const name = this.userService.getUserName();
this.notificationService.show(
`Welcome ${name}`
);
}
Ninguno de los servicios es creado por el componente; Angular los construye e inyecta ambos.
Los servicios y DI están relacionados pero son distintos
Es fácil usar estos dos términos de manera intercambiable, pero describen cosas diferentes. Un servicio es una clase que contiene lógica reutilizable o estado compartido. Inyección de dependencias es el mecanismo que proporciona ese servicio a las partes de la aplicación que lo necesitan:
Service
↓
Provides functionality
Dependency Injection
↓
Provides the service where needed
Component
↓
Uses the service
Aplicado a un servicio concreto:
UserService
↓
Angular DI
↓
User Component
↓
Uses UserService
Mantener las funciones separadas permite cambiar la forma o el lugar en que se proporciona un servicio sin tocar los componentes que lo consumen.
Combinar la inyección de dependencias con señales
La inyección de dependencias se combina naturalmente con señales para gestionar estados reactivos compartidos. El servicio a continuación posee una señal de contador y un método que la actualiza:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = signal(0);
increase() {
this.count.update(value => value + 1);
}
}
Un componente inyecta el servicio como cualquier otro:
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
counterService = inject(CounterService);
}
Su plantilla lee la señal llamándola, y el botón llama al método del servicio:
<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
Increase
</button>
Aquí cooperan tres conceptos:
Dependency Injection
↓
CounterService
↓
Signal
↓
Component
↓
Template
El servicio es el responsable del estado, DI hace que la misma instancia de dicho servicio esté disponible para todos los consumidores, y la señal se encarga de que el template se actualice cuando cambia el valor. Dado que CounterService se proporciona en la raíz, cada componente que lo inyecta ve el mismo conteo. Si se proporcionara en un componente concreto, cada subárbol tendría su propio contador independiente.
Qué te aporta DI
Los beneficios se multiplican a medida que la aplicación crece:
- Organización: los componentes no necesitan saber cómo se construyen sus dependencias ni qué necesitan esas dependencias.
- Reutilización: un solo servicio puede servir a muchos componentes.
Una aplicación de compras como ejemplo
Imagínese una tienda en línea con unos pocos servicios, cada uno con una responsabilidad específica:
ProductService
↓
Gets products from API
↓
CartService
↓
Manages cart state
AuthService
↓
Manages authentication
NotificationService
↓
Displays notifications
Los componentes individuales solo inyectan lo que necesitan; por ejemplo, una barra de navegación podría requerir tanto la autenticación como el estado del carrito:
ProductComponent
↓
ProductService
↓
CartComponent
↓
CartService
Navbar
↓
AuthService
↓
CartService
Angular conecta todo esto entre sí, y cada componente se concentra en su propia interfaz de usuario en lugar de en crear y gestionar los objetos que la sustentan.
El error más común: un servicio sin proveedor
Los principiantes suelen escribir una clase simple:
export class UserService {
}
y luego intentan inyectarla:
userService = inject(UserService);
Dado que no hay ningún proveedor registrado en ningún lugar, Angular no tiene forma de manejar esa clase y la inyección falla con el error de proveedor faltante mencionado anteriormente. Para un servicio que se utilice en toda la aplicación, la solución es agregar el decorador con una registro raíz:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Ahora el inyector raíz sabe cómo crear el servicio. Alternativamente, incluya la clase en el array providers de un componente si desea una instancia limitada al mismo.
Resumen
En Angular moderno, una clase obtiene una dependencia mediante inject():
userService = inject(UserService);
y un servicio se hace disponible en toda la aplicación con:
@Injectable({
providedIn: 'root'
})
La configuración providedIn determina dónde se registra el servicio, y el sistema de inyección jerárquica permite compartir una única instancia en toda la aplicación o asignar instancias separadas a un componente y sus descendientes. El flujo completo de la solicitud es el siguiente:
Component
↓
Requests Dependency
↓
Angular DI
↓
Provider
↓
Service
Y con señales añadidas para el estado reactivo:
Component
↓
inject()
↓
Service
↓
Signal
↓
Reactive UI
Puntos clave
- Los componentes solicitan dependencias; no las crean. Angular encuentra un proveedor y suministra la instancia.
- Llame a
inject()únicamente en un contexto de inyección, como en un inicializador de campo o constructor. providedIn: 'root'genera un singleton creado de forma perezosa y susceptible de eliminación en el árbol, siendo la opción predeterminada adecuada para servicios a nivel de aplicación.- Una entrada
providersa nivel de componente crea una instancia separada para ese componente y su subárbol.
NullInjectorError.