Cancelar requisições ao navegar: AbortController, Signals e RxJS em componentes standalone Angular
Por John Pires · 12 de março de 2026

Cancelar requisições ao navegar: AbortController, Signals e RxJS em componentes standalone
1) Cenário prático do dia a dia
Aplicações SPA frequentemente fazem múltiplas requisições concorrentes enquanto o usuário navega entre rotas. Um componente lista começa a buscar dados; o usuário navega antes do término; a resposta chega e atualiza um estado que não existe mais — ou pior, dispara efeitos que atropelam a nova rota. Isso causa logs ruidosos, exceções por manipular componentes desmontados, e tráfego desnecessário. Precisamos de uma estratégia previsível para cancelar requisições quando a intenção do usuário muda.
Problema observável
Exemplo comum: componente A inicia fetch largo (por exemplo, busca com paginação ou filtro), usuário navega para componente B, mas o callback do fetch ainda atualiza stores ou signals usados por A. Mesmo com unsubscribe, dependendo da API subjacente a requisição pode continuar no servidor, consumindo recursos.
Outro caso: múltiplos cliques dispara N requisições; respostas chegam em ordem indefinida e sobrescrevem o estado. Precisamos controlar ciclo de vida do request atrelado ao ciclo de vida da rota/componente.
Alternativas rápidas
- Confiar no unsubscribe do Observable retornado por HttpClient — funcional, mas menos explícito para APIs fetch ou libs que não obedecem cancelamento por unsubscribe.
- Usar takeUntil com um Subject que emite em ngOnDestroy — padrão RxJS; válido, mas exige disciplina em cada componente.
- AbortController + fetch / fromFetch — cancelamento explícito no nível do browser; claro, funciona mesmo com APIs que não são canceladas por unsubscribe.
Vamos investigar uma abordagem combinada: usar AbortController para garantir cancelamento a nível de rede, RxJS (fromFetch) para integração com streams, e signals em components standalone para estado local previsível. Centralizaremos lógica de cancelamento via um service que opera com o Router para cancelar requests ao navegar.
2) Arquitetura da solução
Objetivos arquiteturais:
- Cancelar requisições ativas quando a navegação começa (NavigationStart).
- Associar controllers ao escopo do componente/rota para cancelamento seletivo.
- Expor uma API simples para serviços de dados (ApiService) para criar Observables canceláveis via AbortSignal.
- Usar signals no componente para refletir loading/data/error sem efeitos colaterais quando a rota muda.
Componentes da arquitetura
- RequestManagerService: cria e registra AbortControllers; cancela todos no NavigationStart ou manualmente.
- ApiService: utilitário para transformar chamadas fetch em Observables RxJS (utilizando fromFetch), aceitando AbortSignal vindo do RequestManagerService.
- Componentes standalone: consomem ApiService, solicitam um controller do RequestManagerService para cada chamada e vinculam o ciclo via signals.
- AppComponent ou provider root: inicializa RequestManagerService para ouvir Router.events e chamar cancelAll automaticamente.
Fluxo de execução
Quando um componente quer buscar dados, pede um controller ao RequestManagerService. O ApiService usa essa signal (controller.signal) ao criar um Observable via fromFetch. O componente faz subscribe, atualiza signals. Se o usuário iniciar navegação, RequestManagerService intercepta Router.events e chama controller.abort() em todos os controllers ativos; Observables emitirão erro/completion, e o componente limpa estado de loading de forma previsível.
3) Código completo com explicações
Vou mostrar três blocos principais: RequestManagerService, ApiService e um componente standalone que usa ambos. Cada bloco terá contexto antes e explicação depois.
RequestManagerService — contexto
Este serviço é singleton (providedIn: 'root'). Ele cria AbortControllers por requisição, mantém um Set para controle e oferece cancelAll() para abortar todas as requisições ativas. Também se inscreve em Router.events para cancelar automaticamente no NavigationStart.
import { Injectable, inject, OnDestroy } from "@angular/core";
import { Router, NavigationStart } from "@angular/router";
@Injectable({ providedIn: 'root' })
export class RequestManagerService implements OnDestroy {
private controllers = new Set<AbortController>();
private router = inject(Router);
constructor() {
this.router.events.subscribe(evt => {
if (evt instanceof NavigationStart) {
this.cancelAll();
}
});
}
createController(): AbortController {
const c = new AbortController();
this.controllers.add(c);
// cleanup when aborted nativamente
const cleanup = () => this.controllers.delete(c);
c.signal.addEventListener('abort', cleanup, { once: true });
return c;
}
cancelAll(): void {
for (const c of Array.from(this.controllers)) {
try { c.abort(); } catch { /* ignore */ }
}
this.controllers.clear();
}
ngOnDestroy(): void {
this.cancelAll();
}
}
O código registra controllers, cancela todos em NavigationStart e remove referências quando abortados. Isso evita leaks e centraliza política de cancelamento.
ApiService — contexto
ApiService oferece métodos para criar Observables utilizando fromFetch (rxjs/fetch). Advantage: aceita AbortSignal, integra com RxJS operators e permite map/error handling no pipeline. Exemplo abaixo mostra um GET com parsing JSON e timeout opcional via operator.
import { Injectable } from "@angular/core";
import { fromFetch } from "rxjs/fetch";
import { switchMap, timeout, map, catchError } from "rxjs/operators";
import { of } from "rxjs";
@Injectable({ providedIn: 'root' })
export class ApiService {
getJson<T>(url: string, signal?: AbortSignal, tms = 15000) {
return fromFetch(url, { signal }).pipe(
timeout(tms),
switchMap(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}),
map(data => data as T),
catchError(err => { throw err; })
);
}
}
Esse serviço não conhece RequestManagerService diretamente — ele recebe o AbortSignal. Isso deixa a API testável e desacoplada.
Standalone Component — contexto
Componente demonstra uso prático: é standalone, usa signals para estados, pede controller e assina o Observable. A vantagem dos signals é evitar setState manual; eles garantem reatividade local simples.
import { Component, signal, effect } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ApiService } from "./api.service";
import { RequestManagerService } from "./request-manager.service";
import { takeUntil } from "rxjs/operators";
@Component({
selector: 'app-items',
standalone: true,
imports: [CommonModule],
template: `
<div>
<h2>Items</h2>
<div *ngIf="loading()">Loading...</div>
<ul>
<li *ngFor="let i of items()">{{i.name}}</li>
</ul>
<div *ngIf="error()" class="error">{{error()}}</div>
</div>
`
})
export class ItemsComponent {
items = signal<any[]>([]);
loading = signal(false);
error = signal<string | null>(null);
constructor(private api: ApiService, private rm: RequestManagerService) {
this.load();
}
load() {
const controller = this.rm.createController();
this.loading.set(true);
this.error.set(null);
// fromFetch Observable - cancela ao controller.abort()
this.api.getJson<any[]>('/api/items', controller.signal)
.subscribe({
next: data => {
this.items.set(data);
this.loading.set(false);
},
error: err => {
// Se foi abortado, err.name === 'AbortError' ou timeout
this.loading.set(false);
if ((err as any).name === 'AbortError') {
this.error.set('Request canceled');
} else {
this.error.set(err.message || String(err));
}
}
});
}
}
Explicação: o componente cria controller por request. Se o usuário navegar, RequestManagerService aborta controller(s), a subscription recebe erro e mantém signals consistentes. Não é necessário ngOnDestroy manual aqui, porque o RequestManagerService oferece cancelamento global; porém você ainda pode abortar controllers específicos se quiser.
4) Tratamento de erros e edge cases
Cancelamento introduz alguns casos especiais que precisam atenção para não confundir usuários ou poluir logs:
1) Diferenciar abort de falha
Quando uma requisição é abortada, geralmente o erro tem name === 'AbortError'. Trate esse caso separadamente para não mostrar stacks ou mensagens de rede ao usuário. Use mensagens simples como "Ação cancelada" ou suprimir notificações em casos de navegação explícita.
Exemplo de checagem:
// dentro do catch/error handler
if ((err as any).name === 'AbortError') {
// comportamento silencioso ou log para desenvolvimento
} else {
// mostrar erro ao usuário
}
2) Timeouts vs Abort
Timeouts (rxjs timeout operator) e AbortController podem conflitar na forma de erros. Timeout lança um TimeoutError; abort lança AbortError. Pense na UX: timeout é falha de rede; abort é intenção do usuário. Trate separadamente e centralize mensagens.
3) Requisições idempotentes vs não idempotentes
Abort cancela a chamada localmente, mas o servidor pode já ter processado parte do request (especialmente para POST/PUT). Para operações críticas, prefira designs idempotentes ou confirmar estado posteriormente. Cancelar não garante rollback no servidor.
4) Leak de controllers sem listener
Se você criar controllers e nunca chamar abort ou esperar a resposta, os controllers podem permanecer no Set do RequestManagerService. Implementamos listener no signal.addEventListener('abort', cleanup) para evitar leaks; mantenha esse padrão.
5) Erros comuns e armadilhas
- Não chamar controller.remove depois de completar pode manter referências e impedir GC — use cleanup listeners.
- Esperar que unsubscribe do Observable sempre aborte a requisição: nem todas as libs fazem isso; fetch e fromFetch com AbortSignal são explícitos.
- Confundir NavigationEnd com NavigationStart: o cancelamento precisa acontecer no start para evitar que a rota nova seja afetada por resultados antigos.
Dica: centralize decisão de cancelamento no serviço de requests; componentes só pedem controllers e não precisam conhecer Router.events. Isso melhora testabilidade.
5) Performance e otimização
Cancelamento reduz tráfego desnecessário e CPU de processamento de callbacks inválidos — ganho direto de performance. Mas há nuances para otimizar:
Batching e deduplicação
Se múltiplos componentes pedem a mesma API simultaneamente, preferir shareReplay ou um cache com deduplicação evita disparar múltiplas requisições. Se usar controllers por requisição, você pode compartilhar o mesmo controller para requests que são logicamente a mesma operação.
Exemplo simples de dedupe no ApiService:
// pseudo: map de url para Observable compartilhado
const inflight = new Map<string, Observable<any>>();
function getCached(url: string, signal?: AbortSignal) {
if (inflight.has(url)) return inflight.get(url)!;
const obs = fromFetch(url, { signal }).pipe(
switchMap(r => r.json()),
shareReplay({ bufferSize: 1, refCount: true })
);
inflight.set(url, obs);
obs.subscribe({ complete: () => inflight.delete(url), error: () => inflight.delete(url) });
return obs;
}
Granularidade de cancelamento
Cancelar tudo em NavigationStart é simples, mas por vezes você quer cancelar apenas requests ligados à rota antiga e manter outros (por exemplo, background sync). Para isso, associe controllers a um escopo (routeId, componentId) no RequestManagerService e permita cancel(scope) em vez de cancelAll().
Impacto no servidor
Abort reduz carga de resposta, mas o servidor pode já ter processado a requisição parcial. Para endpoints pesados, considere endpoint que verifica cabeçalho 'x-client-cancel' ou uma estratégia de idempotency para operações sensíveis.
Conclusão prática
Cancelar requisições ao navegar é essencial para manter UX consistente, evitar efeitos colaterais e reduzir consumo de recursos. A combinação de RequestManagerService (centraliza AbortControllers), ApiService (fromFetch e RxJS) e components standalone com signals resulta em um padrão claro: controllers por request, cancelamento automático no NavigationStart, e tratamento explícito de AbortError.
Práticas recomendadas resumidas:
- Centralize criação e cancelamento de controllers.
- Use fromFetch + AbortSignal ou garantias de cancelamento via unsubscribe para HttpClient.
- Use signals para estado local em components standalone; simplifies reatividade e evita leaks.
- Trate AbortError separadamente e prefira mensagens silenciosas quando o usuário cancela intencionalmente.
- Considere deduplicação e escopos de cancelamento para requests compartilhados.
Implementando esse padrão, você reduz bugs relacionados a requisições tardias, melhora performance e entrega uma experiência mais previsível. Comece centralizando o RequestManagerService e migrando chamadas críticas para ApiService com suporte a AbortSignal; a migração incremental é simples e segura.
Se precisar, posso fornecer uma versão completa do projeto de exemplo (stack: Angular 16+, rxjs, standalone components) com testes unitários que cobrem cancelamento e mensagens de erro.
Assuntos: Angular, rxjs, signals, abortcontroller, standalone, routing
Leia também
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…
Prefetch de dados por rota no Angular com Standalone Components e Signals
Aprenda a prefetch de dados por rota no Angular usando Standalone Components e Signals: combine ResolveFn com um PrefetchStore reativo para reduzir latência…
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…
Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza
Guia prático para usar providers com escopo de rota em Angular standalone: exemplos com Signals, factories que leem params de rota, limpeza via onCleanup e…
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…
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…