Estado HTTP sem transformar tudo em Signals

Estado HTTP no Angular fica mais simples quando o time separa valores de UI, derivações, leituras remotas e fluxos assíncronos antes de escolher Signals ou RxJS. HttpClient continua canônico em produção, e httpResource agora é estável desde o Angular 22, então a pergunta saiu de 'é seguro' para 'onde encaixa.' toSignal segue sendo a ponte pra um stream RxJS existente.

Classifique o estado antes de escolher a ferramenta

A primeira pergunta de review não é "Signals ou RxJS?" É "que tipo de estado essa feature está carregando?"

Um filtro digitado, um botão derivado, uma leitura HTTP, uma ação de reload, um stream WebSocket e estado de domínio compartilhado são problemas diferentes. Quando a mesma ferramenta tenta resolver todos, o resultado costuma ser uma facade cheia de adaptadores que ninguém quer depurar.

Formato do estadoExemploPonto de partida
Valor local de UIaba selecionada, linha expandida, filtro digitadosignal
Derivação síncronapode salvar, contagem filtrada, label de exibiçãocomputed
Leitura remota simplesperfil por id, tela de configuraçõesHttpClient + AsyncPipe ou toSignal
Fluxo assíncrono no tempobusca, retry, polling, cancelamentoRxJS em uma facade ou store
Estado de domínio compartilhadocarrinho, auth, permissões, fluxo em várias telasStore/facade existente, com Signals nas leituras do componente

Mantenha o service de API simples

Preserve primeiro um service de API simples. Ele deve descrever o contrato com o backend, usar HttpClient e evitar decidir como todo componente vai consumir estado.

Esse service não fica antigo só porque retorna Observable. Ele é um contrato estável. A modernização normalmente mora no componente ou na facade, onde a UI precisa de valor atual, loading, retry ou cancelamento.

Service de API simplests
@Injectable({ providedIn: 'root' })
export class OrdersApi {
  private readonly http = inject(HttpClient);

  listOrders() {
    return this.http.get<Order[]>('/api/orders');
  }
}

Uma facade que mistura ferramentas de propósito

Uma facade de feature pode deixar cada ferramenta cumprir um papel. Um signal local registra a intenção de reload. RxJS modela request, loading, erro e replay. toSignal entrega um estado atual para o componente ler sem código manual de subscription.

Não esconda o stream RxJS como se ele fosse dívida legada. Ele é o lugar certo para lifecycle de request, fallback e semântica de cache. Signals melhoram o contrato consumido pelo template.

O valor inicial de refresh dispara a primeira request, e reload() incrementa esse valor depois. O stream interno controla loading para cada request, sem depender de um startWith perdido antes do switchMap.

Se o produto precisa manter linhas antigas visíveis durante um reload, codifique isso em OrdersState; não deixe a UI inferir essa decisão a partir de shareReplay.

Facade de pedidosts
type OrdersState =
  | { status: 'loading'; data: Order[]; error: null }
  | { status: 'ready'; data: Order[]; error: null }
  | { status: 'empty'; data: Order[]; error: null }
  | { status: 'error'; data: Order[]; error: string };

const loadingState: OrdersState = {
  status: 'loading',
  data: [],
  error: null,
};

@Injectable()
export class OrdersFacade {
  private readonly api = inject(OrdersApi);
  private readonly refresh = signal(0);

  readonly state$ = toObservable(this.refresh).pipe(
    switchMap(() =>
      this.api.listOrders().pipe(
        map((data) => ({
          status: data.length > 0 ? 'ready' : 'empty',
          data,
          error: null,
        }) satisfies OrdersState),
        startWith(loadingState),
        catchError(() => of({
          status: 'error',
          data: [],
          error: 'Failed to load orders',
        } satisfies OrdersState))
      )
    ),
    shareReplay({ bufferSize: 1, refCount: true })
  );

  readonly state = toSignal(this.state$, {
    initialValue: loadingState,
  });

  readonly orders = computed(() => this.state().data);
  readonly isLoading = computed(() => this.state().status === 'loading');
  readonly error = computed(() => this.state().error);

  reload() {
    this.refresh.update((value) => value + 1);
  }
}

O template deve manter todos os estados visíveis

Exemplos fracos pulam justamente os estados que doem em produção: loading inicial, resultado vazio, erro no reload e retry. Alguns branches extras no template custam menos do que uma demo que só funciona no caminho feliz.

O componente não precisa saber se a facade usou RxJS, store ou toSignal internamente. Ele precisa de um contrato de estado claro e identidade estável na lista.

O template nomeia o estado atual com @let, então cada branch lê de um valor discriminado em vez de repetir chamadas de signal e non-null assertions.

Contrato de template com estado explícitohtml
@let state = facade.state();

@switch (state.status) {
  @case ('loading') {
    <app-orders-skeleton />
  }
  @case ('error') {
    <app-inline-error
      [message]="state.error"
      (retry)="facade.reload()"
    />
  }
  @case ('empty') {
    <app-empty-orders />
  }
  @case ('ready') {
    @for (order of state.data; track order.id) {
      <app-order-row [order]="order" />
    }
  }
}

Onde `httpResource` encaixa agora que é estável

httpResource encaixa numa leitura estreita porque empacota valor, loading, erro, reload e cancelamento em torno de dependências reativas.

O Angular 22 o tornou estável, então não é mais sobre risco, é sobre encaixe. Ele inicia requests de forma eager quando as dependências permitem, cancela request pendente quando dependências mudam, e é feito pra leituras. Não deveria virar seu default pra mutações como POST ou PUT, que seguem no HttpClient.

Leitura de baixo risco com httpResourcets
export class CustomerPanelComponent {
  readonly customerId = input.required<string>();

  readonly customer = httpResource(() =>
    `/api/customers/${this.customerId()}`
  );

  readonly displayName = computed(() => {
    if (!this.customer.hasValue()) return 'Customer not loaded';
    return this.customer.value().name;
  });
}
Proteja leituras de value antes de renderizarhtml
@if (customer.hasValue()) {
  <app-customer-card [customer]="customer.value()" />
} @else if (customer.error()) {
  <app-inline-error
    message="Could not load customer"
    (retry)="customer.reload()"
  />
} @else if (customer.isLoading()) {
  <app-customer-skeleton />
}

Regra de produção para apps maduros

Para apps Angular maduros, a regra de produção é firme: mantenha services de API simples, mantenha RxJS onde tempo e cancelamento importam, exponha Signals onde o componente precisa de estado atual e use as APIs de resource agora estáveis nas leituras onde elas merecem o lugar.

Isso entrega Angular moderno ao time sem transformar estado HTTP em outra campanha de reescrita.

Artefato para reaproveitar

Checklist de review para estado HTTP

  • Nomeie o formato do estado antes de escolher Signals, RxJS, store ou resource.
  • Mantenha services de API focados no contrato com o backend.
  • Preserve semântica de loading, vazio, erro, retry e cache.
  • Decida se o reload limpa dados antigos ou os mantém visíveis, depois codifique isso no contrato de estado.
  • Use toSignal nas leituras do componente, não como motivo para apagar todo Observable.
  • Use resource e httpResource em leituras agora que são estáveis no v22; mantenha mutações no HttpClient.

Fontes consultadas

Modern Angular Playbook

Este artigo é um play.

O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.

Você recebe os dois PDFs por email, pela lista do Dojo IA.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Guia · 7 min

Signals sem briga com RxJS

Uma fronteira de produção para Signals, computed, streams RxJS e interop, sem reescrever por estética uma arquitetura que já funciona.

Ler artigo →

Erro · 7 min

NG0950: lendo um input obrigatório antes do Angular preencher

Por que o NG0950 do Angular dispara quando um signal input obrigatório é lido antes do binding, quando os inputs ficam disponíveis de fato, e os três consertos: ler no ngOnInit / computed / effect, dar um valor padrão, ou fazer o binding no pai.

Ler artigo →

Guia · 10 min

O código que quebra quando Angular fica zoneless

Um guia técnico para revisar timers, subscriptions, forms, callbacks de terceiros e testes antes de um spike zoneless no Angular.

Ler artigo →