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:
- Se precisa de isolamento e destruí-lo ao sair da rota → provider por rota.
- Se precisa compartilhar entre várias rotas/abas → singleton/store.
- 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:
- Unit tests para serviços garantindo que timers/subscriptions são limpos ao chamar DestroyRef (mock do DestroyRef ou instanciar e chamar onDestroy).
- 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
Signals + RxJS em rotas lazy com Standalone Components e Route Providers: modo errado vs modo certo
Padrões práticos para combinar Signals e RxJS em rotas lazy com standalone components e route providers: comparativo, erros comuns e implementação correta…
Sincronizar filtros na URL com Angular: Signals + Router + RxJS (para iniciantes)
Aprenda a sincronizar filtros com a URL no Angular usando Signals, Router e RxJS: passo a passo para iniciantes, evitando loops de navegação, aplicando…
Wizard multi-step com rotas filhas, Signals e Standalone Components
Tutorial prático e direto para criar um wizard multi-step em Angular: rotas filhas para cada etapa, gerenciamento de estado com Signals e componentes…
Cancelar requisições ao navegar: AbortController, Signals e RxJS em componentes standalone Angular
Padrões práticos para cancelar requisições HTTP ao navegar em Angular: combinar AbortController, rxjs/fromFetch e signals em components standalone para evitar…
Prefetch inteligente de rotas com Signals e RxJS em Standalone Components
Prefetch inteligente para Angular: combine Signals para estado e RxJS para orquestração de intents (hover, focus, intersection). Exemplo prático com…
Migrando BehaviorSubject para Signal em formulários Angular — passo a passo
Guia prático para migrar BehaviorSubject para Signal em formulários Angular: exemplos errados e corretos, patterns com standalone components, DI e routing, e…