Destino Dev

Blog

Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza

Por John Pires · 12 de março de 2026

Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza

1) Quando e por que usar

Contexto prático

Em aplicações Angular modernas com componentes standalone e Signals, nem todo estado precisa ser global. Providers com escopo de rota permitem criar instâncias com ciclo de vida ligado à rota: útil para formulários com estado temporário, contadores locais, caches por rota, e integrações que devem morrer ao navegar. Usar escopo de rota reduz vazamento de memória, melhora isolamento e simplifica testes.

Benefícios concretos

As vantagens práticas são diretas: instâncias destruidas quando a rota é descartada, possibilidade de inicializar serviços com dados do ActivatedRoute via factory, e menor acoplamento entre rotas. Em apps com lazy loading e muitos estados temporários, providers por rota diminuem a complexidade do gerenciamento manual de ciclo de vida.

Este capítulo mostra quando optar por um provider de rota em vez de singleton ou providedIn: 'root':

  • Estado exclusivo por instância de rota (ex.: editor de item).
  • Recursos que precisam ser inicializados com parâmetros da rota.
  • Serviços que mantêm timers, websockets ou subscriptions locais.

Exemplo rápido de quando evitar: serviços que representam caches globais, autenticação, ou recursos reutilizados por toda a aplicação. Nesses casos, prefira singletons.

2) Comparação com alternativas

Singletons (providedIn: 'root')

Singletons facilitam compartilhamento de estado, mas exigem limpeza manual quando usados para recursos temporários. Se você usar providedIn: 'root' para um serviço que abre websockets por usuário, precisa agregar lógica de referência/contagem ou gerenciamento explícito do ciclo de vida para evitar vazamentos.

Store local ou global (NgRx / Akita)

Stores são excelentes para previsibilidade e time-travel, mas têm overhead organizacional. Para estados simples e efêmeros, criar um provider por rota é mais simples e reduz boilerplate. Use store quando o estado for compartilhado amplamente ou quando precisamos de ferramentas de depuração sofisticadas.

Resumo prático:

  1. Se precisa de isolamento e destruí-lo ao sair da rota → provider por rota.
  2. Se precisa compartilhar entre várias rotas/abas → singleton/store.
  3. Se precisa de histórico/undo/redo → store.

3) Implementação progressiva

Exemplo 1 — Provider de rota simples com Signals

O primeiro exemplo mostra um serviço simples com Signals que é fornecido apenas para a rota '/counter'. A instância é criada quando a rota é ativada e destruída ao sair.

Contexto: CounterService mantém um signal com contador e um timer que incrementa automaticamente. Precisamos garantir que o timer seja cancelado quando a rota for destruída.

/* counter.service.ts */
import { Injectable, signal, effect, onCleanup, inject, DestroyRef } from '@angular/core';

@Injectable()
export class CounterService {
  count = signal(0);
  private intervalId?: number;

  constructor() {
    // Um effect que inicia um timer e garante cleanup via onCleanup
    effect(() => {
      // inicia timer
      this.intervalId = window.setInterval(() => this.count.update(c => c + 1), 1000);

      // quando o effect for destruido, limpa o timer
      onCleanup(() => {
        if (this.intervalId) {
          clearInterval(this.intervalId);
        }
      });
    });
  }

  increment() { this.count.update(c => c + 1); }
  reset() { this.count.set(0); }
}

O que acontece depois: o effect cria o timer na criação do serviço. Quando o injector da rota for destruído, o effect é limpo e onCleanup garante que o timer seja cancelado. Não precisa implementar OnDestroy manualmente.

Código de rota e componente:

/* app.routes.ts */
import { Routes } from '@angular/router';
import { CounterComponent } from './counter.component';
import { CounterService } from './counter.service';

export const routes: Routes = [
  { path: 'counter', component: CounterComponent, providers: [CounterService] }
];

/* counter.component.ts (standalone) */
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CounterService } from './counter.service';

@Component({
  standalone: true,
  imports: [CommonModule],
  selector: 'app-counter',
  template: `
    <div>count: {{ svc.count() }}</div>
    <button (click)="svc.increment()">+1</button>
    <button (click)="svc.reset()">reset</button>
  `
})
export class CounterComponent {
  svc = inject(CounterService);
}

O que acontece depois: quando o usuário navegar para '/counter', o CounterService é criado e o contador automático começa. Ao navegar embora, o serviço é destruído e o timer é limpo automaticamente.

Exemplo 2 — Provider com factory usando parâmetros de rota

Contexto: inicializar serviço com valor vindo da rota, por exemplo '/counter/10'. Aqui usamos useFactory com inject(ActivatedRouteSnapshot) para capturar parâmetro inicial e criar a instância configurada.

/* app.routes.ts (factory example) */
import { Routes, ActivatedRouteSnapshot, RouterModule } from '@angular/router';
import { CounterComponent } from './counter.component';
import { CounterService } from './counter.service';
import { inject } from '@angular/core';

export const routes: Routes = [
  {
    path: 'counter/:start',
    component: CounterComponent,
    providers: [
      {
        provide: CounterService,
        useFactory: () => {
          const route = inject(ActivatedRouteSnapshot);
          const start = Number(route.paramMap.get('start')) || 0;
          const s = new CounterService();
          s.count.set(start);
          return s;
        }
      }
    ]
  }
];

Depois: a rota constrói a instância com o valor inicial da rota. Isso evita que o componente precise ler params e repassar para o serviço, simplificando testes.

4) Integração com ecossistema real

Signals + RxJS: transformar Observable em signal com cleanup

Integração típica: o serviço precisa consumir um Observable (ex.: WebSocket ou HTTP em polling) e expor signal. Uma solução robusta é assinar o Observable dentro do service e usar DestroyRef para cancelar a subscription quando o provider for destruído.

/* live-data.service.ts */
import { Injectable, signal, inject, DestroyRef } from '@angular/core';
import { interval, map } from 'rxjs';

@Injectable()
export class LiveDataService {
  data = signal<number | null>(null);
  private destroyRef = inject(DestroyRef);

  constructor() {
    const sub = interval(1000).pipe(map(i => i * 2)).subscribe(v => this.data.set(v));

    // garante cancelamento ao destruir o provider da rota
    this.destroyRef.onDestroy(() => sub.unsubscribe());
  }
}

O que acontece depois: a subscription ao interval é criada na construção e é automaticamente cancelada quando a rota (e seu injector) é destruído. Isso evita leak.

Uso com Guards, Resolvers e lazy loading

Você pode usar providers de rota junto com resolvers para preencher estado antes da ativação do componente. Também é válido definir providers em rotas de lazy-loaded modules; o injector da rota lazy será filho do root, preservando isolamento. Para Guards que dependem de providers locais, declare o provider na mesma rota ou rota pai apropriada para que o guard também consiga injetá-lo.

Exemplo rápido: uma rota com resolver que injeta um serviço escopado para popular o signal antes de ativação permite que o componente somente leia valores prontos sem lógica de carregamento.

5) Checklist de qualidade para produção

Regras práticas antes do deploy

Checklist mínimo para garantir trabalho confiável em produção:

  • O serviço não mantém subscriptions abertas sem cleanup (use DestroyRef.onDestroy ou onCleanup nos effects).
  • Providers por rota usados apenas quando realmente precisarem de isolamento.
  • Evitar dependências circulares entre providers de rota e singletons.
  • Documentar contrato do provider (se ele precisa ser resetado/serializado).

Testes e monitoramento

Recomendações práticas de teste:

  1. Unit tests para serviços garantindo que timers/subscriptions são limpos ao chamar DestroyRef (mock do DestroyRef ou instanciar e chamar onDestroy).
  2. Teste de integração: navegar para rota e para outra rota e confirmar que não existem tasks pendentes (ex.: no Chrome devtools).
Tip: use perf tools do navegador e library como zone-less devtools para detectar timers e intervals que não foram cancelados.

Erros comuns e armadilhas

Listo alguns erros que já vi em produção e como evitá-los.

  • Assumir que providedIn: 'root' sempre é ok — serviços com timers/WS precisam cleanup; prefira scoping por rota.
  • Dependências que precisam do ActivatedRoute no constructor — se usar useFactory e inject(ActivatedRouteSnapshot), confirmar que o token correto é injetável naquele nível de injector.
  • Esquecer onCleanup dentro de effect — efeitos criam listeners; sempre limpar recursos neles.

Exemplo de armadilha: realizar subscribe() em um effect sem onCleanup. Isso mantém a subscription mesmo após a rota ser destruída.

/* antipattern.service.ts */
import { Injectable, effect } from '@angular/core';
import { interval } from 'rxjs';

@Injectable()
export class AntiPatternService {
  constructor() {
    // MAU: assinando sem cleanup
    interval(1000).subscribe(v => console.log('tick', v));
  }
}

Depois: esse padrão vazará se o provider for criado várias vezes sem cancelar a assinatura. Sempre use onCleanup ou DestroyRef.onDestroy.

Conclusão prática

Providers com escopo de rota em Angular standalone oferecem ferramenta poderosa de isolamento de estado, especialmente quando combinados com Signals. Eles simplificam a criação de serviços efêmeros e permitem inicializar instâncias a partir de parâmetros de rota com useFactory/inject. O ponto chave para produção é garantir limpeza explícita: use onCleanup em effects e DestroyRef.onDestroy para subscriptions/recursos externos.

Resumo rápido de ação:

  • Prefira provider por rota para estado efêmero ligado ao ciclo de vida da rota.
  • Use signals para expor estado reativo e effects + onCleanup para lógica internal.
  • Cancele subscriptions com DestroyRef.onDestroy para garantir que não haja vazamentos.

Implementando os padrões mostrados você reduz complexidade, evita leaks e melhora previsibilidade na navegação. Adote as práticas com testes que verifiquem criação/remoção das instâncias e monitore timers/handlers no ambiente real.

Exemplos completos e testáveis estão acima; pegue o padrão que se encaixa no seu caso e aplique nas rotas que mantêm recursos temporários.

Assuntos: Angular, signals, standalone, routing, dependency-injection, rxjs

Leia também