Destino Dev

Blog

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:

  1. Cancelar requisições ativas quando a navegação começa (NavigationStart).
  2. Associar controllers ao escopo do componente/rota para cancelamento seletivo.
  3. Expor uma API simples para serviços de dados (ApiService) para criar Observables canceláveis via AbortSignal.
  4. 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