En esta página
Servicios e inyección de dependencias
Qué es un servicio?
Un servicio es una clase que encapsula lógica reutilizable: acceso a datos, estado compartido, utilidades, etc. Es la forma de separar la lógica de negocio de los componentes.
Crear un servicio
Usa el decorador @Injectable con providedIn: 'root' para crear un servicio singleton:
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ContadorService {
private readonly _valor = signal(0);
readonly valor = this._valor.asReadonly();
incrementar(): void {
this._valor.update(v => v + 1);
}
}Con providedIn: 'root', Angular crea una única instancia del servicio para toda la aplicación.
Inyectar un servicio
Usa la función inject() para obtener una instancia del servicio:
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { ContadorService } from './contador.service';
@Component({
selector: 'app-mi-componente',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Valor: {{ contador.valor() }}</p>
<button (click)="contador.incrementar()">+1</button>
`,
})
export class MiComponente {
readonly contador = inject(ContadorService);
}Jerarquia de inyectores
Angular tiene una jerarquia de inyectores. Cada nivel puede proveer sus propias instancias:
Root Injector (providedIn: 'root')
└─ Component Injector (providers: [...])
└─ Child Component InjectorServicio a nivel de componente
Si necesitas una instancia única por componente (no singleton), usa providers:
@Component({
selector: 'app-formulario',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FormStateService], // Nueva instancia por cada <app-formulario>
template: `...`,
})
export class Formulario {
private readonly formState = inject(FormStateService);
}Patron: estado privado, lectura pública
Un patrón fundamental es mantener el signal mutable privado y exponer una versión de solo lectura:
@Injectable({ providedIn: 'root' })
export class AuthService {
// Privado: solo el servicio puede modificar
private readonly _usuario = signal<Usuario | null>(null);
// Público: componentes solo pueden leer
readonly usuario = this._usuario.asReadonly();
readonly estaAutenticado = computed(() => this._usuario() !== null);
login(datos: Credenciales): void {
// ... lógica de autenticación
this._usuario.set(usuarioAutenticado);
}
logout(): void {
this._usuario.set(null);
}
}InjectionToken para valores
Para inyectar valores que no son clases, usa InjectionToken:
import { InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
// Proveer en la config de la app
export const appConfig = {
providers: [
{ provide: API_URL, useValue: 'https://api.ejemplo.com' },
],
};
// Inyectar en un servicio
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly baseUrl = inject(API_URL);
}inject() en funciones
inject() también funciona en funciones que se ejecutan en contexto de inyección, como guards y resolvers:
export function authGuard(): boolean {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.estaAutenticado()) return true;
router.navigate(['/login']);
return false;
}Buenas prácticas
- Un servicio, una responsabilidad — No crees servicios gigantes
- Exponer readonly — Usa
.asReadonly()para signals publicos - inject() sobre constructor — Más legible y flexible
- providedIn: 'root' — Para la mayoria de servicios
Práctica
- Crea un servicio de favoritos: Implementa un
FavoritosServiceconprovidedIn: 'root', un signal privado para la lista y metodosagregar(),eliminar()yesFavorito(). Expone la lista comoasReadonly(). - Inyecta en dos componentes: Usa
inject(FavoritosService)en dos componentes diferentes y verifica que comparten la misma instancia (agregar en uno debe reflejarse en el otro). - Crea un InjectionToken: Define un
InjectionToken<string>para una URL base de API, proveelo en la configuracion de la app y usalo en un servicio coninject().
En la siguiente leccion aprenderemos routing avanzado: lazy loading, guards, resolvers y layouts anidados.
import { Injectable, inject, signal, computed } from '@angular/core';
// --- Modelo ---
interface Producto {
id: number;
nombre: string;
precio: number;
}
interface ItemCarrito {
producto: Producto;
cantidad: number;
}
// --- Servicio singleton (providedIn: 'root') ---
@Injectable({ providedIn: 'root' })
export class CarritoService {
private readonly items = signal<ItemCarrito[]>([]);
readonly itemsCarrito = this.items.asReadonly();
readonly totalItems = computed(
() => this.items().reduce((sum, item) => sum + item.cantidad, 0)
);
readonly totalPrecio = computed(
() => this.items().reduce(
(sum, item) => sum + item.producto.precio * item.cantidad, 0
)
);
agregar(producto: Producto): void {
this.items.update(lista => {
const existente = lista.find(i => i.producto.id === producto.id);
if (existente) {
return lista.map(i =>
i.producto.id === producto.id
? { ...i, cantidad: i.cantidad + 1 }
: i
);
}
return [...lista, { producto, cantidad: 1 }];
});
}
eliminar(productoId: number): void {
this.items.update(lista =>
lista.filter(i => i.producto.id !== productoId)
);
}
vaciar(): void {
this.items.set([]);
}
}
Inicia sesión para guardar tu progreso