Modern Angular
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 estado | Exemplo | Ponto de partida |
|---|---|---|
| Valor local de UI | aba selecionada, linha expandida, filtro digitado | signal |
| Derivação síncrona | pode salvar, contagem filtrada, label de exibição | computed |
| Leitura remota simples | perfil por id, tela de configurações | HttpClient + AsyncPipe ou toSignal |
| Fluxo assíncrono no tempo | busca, retry, polling, cancelamento | RxJS em uma facade ou store |
| Estado de domínio compartilhado | carrinho, auth, permissões, fluxo em várias telas | Store/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.
@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.
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.
@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.
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;
});
}@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
toSignalnas leituras do componente, não como motivo para apagar todo Observable. - Use
resourceehttpResourceem leituras agora que são estáveis no v22; mantenha mutações noHttpClient.
Fontes consultadas
- https://angular.dev/guide/http
- https://angular.dev/api/common/http/httpResource
- https://angular.dev/guide/signals/resource
- https://angular.dev/api/core/rxjs-interop/toSignal
- https://angular.dev/api/core/rxjs-interop/toObservable
- https://angular.dev/guide/signals
- https://angular.dev/roadmap
- https://blog.angular.dev/announcing-angular-v20-b5c9c06cf301
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.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →