En esta página

Servicios e inyección de dependencias

15 min lectura TextoCap. 3 — Arquitectura

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 Injector

Servicio 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

  1. Un servicio, una responsabilidad — No crees servicios gigantes
  2. Exponer readonly — Usa .asReadonly() para signals publicos
  3. inject() sobre constructor — Más legible y flexible
  4. providedIn: 'root' — Para la mayoria de servicios

Práctica

  1. Crea un servicio de favoritos: Implementa un FavoritosService con providedIn: 'root', un signal privado para la lista y metodos agregar(), eliminar() y esFavorito(). Expone la lista como asReadonly().
  2. 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).
  3. 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 con inject().

En la siguiente leccion aprenderemos routing avanzado: lazy loading, guards, resolvers y layouts anidados.

inject() sobre constructor
Prefiere inject(MiServicio) sobre la inyección por constructor. Es más conciso, funciona con herencia y permite inyectar en funciones fuera de clases.
Cuidado con providedIn
Usa providedIn: 'root' para servicios singleton. Si necesitas una instancia por componente, usa el array providers del decorador @Component.
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([]);
  }
}