Artur Bixiga
Voltar para o blog
Resource API do Angular para Busca de Dados Simplificada

Resource API do Angular para Busca de Dados Simplificada

Além do async Pipe: Um Mergulho Profundo na Nova Resource API do Angular para Busca de Dados Simplificada

A Evolução do Estado Assíncrono no Angular

No mundo do desenvolvimento front-end moderno, poucos desafios são tão universais quanto buscar dados e gerenciar seu ciclo de vida. É a base de qualquer aplicação web dinâmica. Por anos, desenvolvedores Angular confiaram em um conjunto de ferramentas poderoso e testado em batalha para essa tarefa: o serviço HttpClient combinado com o rico ecossistema de RxJS Observables. Essa combinação é robusta, flexível e capaz de lidar com cenários assíncronos incrivelmente complexos. No entanto, para o caso de uso mais comum—solicitar dados de um endpoint e exibi-los—esse padrão frequentemente leva a um boilerplate previsível e verboso.

O problema clássico é o gerenciamento manual de estado. Uma única operação lógica, como buscar uma lista de produtos, força os desenvolvedores a manipular múltiplas variáveis de estado. Tipicamente, isso envolve declarar flags ou signals separados para isLoading, error e os dados em si. Embora funcional, essa abordagem polui a lógica do componente, aumenta a superfície para bugs e torna o código mais difícil de entender à primeira vista. Cada componente se torna uma máquina de estados em miniatura, com desenvolvedores orquestrando manualmente as transições entre estados de loading, sucesso e erro.

Esse contexto é crucial para entender a importância da recente mudança de paradigma do Angular. A introdução dos Signals marcou a primeira fase de uma grande evolução, repensando o modelo de reatividade do framework desde a base. Os Signals fornecem uma primitiva simples, poderosa e síncrona para gerenciamento de estado. Eles permitem que o framework entenda, com precisão cirúrgica, quais partes da UI dependem de quais partes do estado, habilitando detecção de mudanças refinada e pavimentando o caminho para um futuro livre do Zone.js.

No entanto, essa revolução síncrona criou uma nova questão. Com um sistema de gerenciamento de estado construído sobre primitivas síncronas, como os desenvolvedores devem lidar com a natureza inerentemente assíncrona da busca de dados? É aqui que a nova e experimental Resource API do Angular emerge como o "elo perdido" crítico. É a resposta oficial, fornecida pelo framework, para conectar o mundo síncrono dos Signals com operações assíncronas. A Resource API não é meramente uma nova utilidade; é o próximo passo lógico na evolução do Angular, uma consequência direta e necessária da introdução dos Signals. Ela resolve o descompasso de impedância entre o novo modelo de reatividade síncrona e o HttpClient baseado em Observable assíncrono, tornando toda a história de gerenciamento de estado mais coesa e declarativa.

Além disso, a introdução da API traz clareza estratégica aos papéis de diferentes ferramentas reativas dentro do ecossistema Angular. A filosofia oficial de design esclarece que RxJS Observables se destacam na modelagem de eventos ao longo do tempo—streams complexos de interações de usuário, WebSockets ou cadeias intricadas de tarefas assíncronas. Em contraste, a nova primitiva resource é construída especificamente para estado derivado assincronamente—o padrão comum de buscar um valor que depende de outras entradas reativas. Isso não é uma substituição do RxJS, mas uma especialização. A Resource API lida elegantemente com o onipresente padrão de estado "buscar-e-exibir", liberando os desenvolvedores para usar a poderosa biblioteca de operadores RxJS para a orquestração complexa baseada em eventos onde ela realmente brilha.

O Cenário Anterior: Uma Revisão do Gerenciamento de Estado Tradicional

Para apreciar completamente a elegância e o poder da Resource API, é essencial primeiro estabelecer uma linha de base clara. Vamos examinar a "maneira antiga" de lidar com um cenário comum: um componente que busca uma lista de produtos de uma API, permitindo que um usuário pesquise por itens específicos.

Em uma abordagem tradicional baseada em signals, sem a Resource API, a lógica do componente seria responsável por gerenciar manualmente todo o ciclo de vida da requisição de dados. Isso envolve criar várias instâncias de WritableSignal para rastrear os vários estados da operação.

Aqui está um exemplo completo e bem comentado de como esse componente poderia ser:

import { Component, inject, signal, effect } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from './product.model';

@Component({
  selector: 'app-product-list-before',
  //... component setup
})
export class ProductListBeforeComponent {
  private http = inject(HttpClient);

  // Gerenciamento de Estado: Três signals separados para uma operação lógica
  products = signal<Product[]>([]);
  isLoading = signal<boolean>(false);
  error = signal<string | null>(null);

  // Entrada do usuário para a busca
  searchQuery = signal<string>('');

  constructor() {
    // Precisamos de um effect para reagir às mudanças na query de busca
    effect(() => {
      const query = this.searchQuery();
      this.fetchProducts(query);
    });
  }

  fetchProducts(query: string): void {
    // 1. Definir manualmente o estado de loading como true
    this.isLoading.set(true);
    this.error.set(null);
    this.products.set([]); // Limpar resultados anteriores

    this.http.get<Product[]>(`/api/products?q=${query}`).subscribe({
      next: (data) => {
        // 2. Em caso de sucesso, definir os dados e resetar o estado de loading
        this.products.set(data);
        this.isLoading.set(false);
      },
      error: (err) => {
        // 3. Em caso de erro, definir a mensagem de erro e resetar o estado de loading
        this.error.set('Falha ao buscar produtos. Por favor, tente novamente.');
        this.isLoading.set(false);
      },
      // Nota: o handler 'complete' é frequentemente redundante aqui
    });
  }

  onSearch(event: Event) {
    const input = event.target as HTMLInputElement;
    this.searchQuery.set(input.value);
  }
}

A lógica correspondente do template então precisaria verificar esses signals individualmente para renderizar a UI correta para cada estado:

<input (input)="onSearch($event)" placeholder="Pesquisar produtos..." />

@if (isLoading()) {
<p>Carregando...</p>
} @else if (error()) {
<p class="error-message">{{ error() }}</p>
} @else if (products().length > 0) {
<ul>
  @for (product of products(); track product.id) {
  <li>{{ product.name }}</li>
  }
</ul>
} @else {
<p>Nenhum produto encontrado.</p>
}

Uma análise cuidadosa desse padrão tradicional revela várias desvantagens claras:

  • Boilerplate Excessivo: O problema mais óbvio é a necessidade de pelo menos três variáveis de estado separadas (products, isLoading, error) para gerenciar o que é conceitualmente uma única operação assíncrona.
  • Gerenciamento de Estado Imperativo: O desenvolvedor é responsável por definir e redefinir manual e corretamente as flags isLoading e error em cada caminho de código possível (next, error). Esquecer de definir isLoading como false no handler de erro é uma fonte comum de bugs.
  • Complexidade Aumentada: A lógica central do componente está profundamente entrelaçada com a mecânica da operação assíncrona. Isso viola o Princípio da Responsabilidade Única; o componente é forçado a ser um gerenciador do processo de busca de dados em vez de simplesmente um apresentador de seu estado. Esse acoplamento forte torna o componente mais frágil e mais difícil de ler, testar e manter ao longo do tempo.

A Primitiva resource: Uma Abordagem Unificada para Estado Assíncrono

A função resource() de @angular/core é o bloco de construção fundamental da nova API, projetada para resolver os problemas da abordagem tradicional. Seu propósito central é pegar uma operação assíncrona e encapsular todo o seu ciclo de vida—pending, resolved e rejected—em um único objeto unificado e reativo.

Para criar um resource, você chama a função resource() com um objeto ResourceOptions. Este objeto tem duas propriedades primárias que trabalham em conjunto: params e loader.

  • params: Esta propriedade é uma computação reativa. É uma função que você fornece, que retorna um valor de parâmetro necessário para sua operação assíncrona. Crucialmente, ela se comporta como um signal computed: sempre que qualquer signal que é lido dentro da função params muda, a função params reavalia e produz um novo valor. Este novo valor então automaticamente dispara a função loader.
  • loader: Esta é a função assíncrona onde o trabalho real acontece, como fazer uma chamada fetch para uma API. Ela recebe um objeto contendo o último valor produzido pela computação params e deve retornar uma Promise que resolve com os dados finais.

Agora, vamos refatorar o componente de lista de produtos da seção anterior usando a primitiva resource(). O contraste é dramático. Os múltiplos signals de estado e a lógica de subscription manual no effect são completamente eliminados.

import { Component, signal } from '@angular/core';
import { resource } from '@angular/core'; // API Experimental
import { Product } from './product.model';

@Component({
  selector: 'app-product-list-after',
  //... component setup
})
export class ProductListAfterComponent {
  // Entrada do usuário para a busca
  searchQuery = signal<string>('');

  // Um único resource encapsula todo o estado e lógica
  productsResource = resource({
    // 1. 'params' define o gatilho reativo para o loader
    params: () => ({ query: this.searchQuery() }),

    // 2. 'loader' executa o trabalho assíncrono
    loader: async ({ params }) => {
      const response = await fetch(`/api/products?q=${params.query}`);
      if (!response.ok) {
        // Erros são tratados lançando exceções, assim como em uma função async padrão
        throw new Error('Falha ao buscar produtos');
      }
      return response.json() as Promise<Product[]>;
    },
  });

  onSearch(event: Event) {
    const input = event.target as HTMLInputElement;
    this.searchQuery.set(input.value);
  }
}

Este código refatorado não é apenas mais curto; é fundamentalmente mais declarativo e robusto. A separação arquitetural entre params e loader impõe um fluxo de dados claro e previsível. A regra é simples: o loader executa se e somente se a função params emite um valor novo e distinto. Essa separação também melhora muito a testabilidade.

Também é importante entender uma escolha de design chave: a própria função loader é um escopo untracked. Isso significa que se você ler um signal dentro do loader que não foi também lido dentro da função params, uma mudança nesse signal não re-dispararia o loader. Esta é uma decisão deliberada para prevenir comportamento confuso ou imprevisível. Ela reforça a clara separação de responsabilidades: params define o que buscar, e loader define como buscar.

Consumindo o Resource: Lógica de UI Declarativa

Uma vez que um resource é criado, o objeto que ele retorna não são os dados em si, mas um container de signals que representam o estado atual da operação assíncrona. Consumir esse estado em seu componente e template é simples e poderoso.

O objeto resource expõe várias propriedades signal chave que você usará para conduzir sua UI:

  • value(): Um signal que contém os dados resolvidos com sucesso. É importante notar que ler este signal quando o resource está em estado de erro lançará um erro.
  • error(): Um signal que contém o objeto de erro se a requisição falhou; caso contrário, é undefined.
  • isLoading(): Um signal booleano que é true enquanto a função loader está executando.
  • status(): Um signal que fornece um status mais granular como uma constante string: 'idle', 'loading', 'reloading', 'resolved' ou 'error'.
  • hasValue(): Uma função booleana reativa crucial que também age como um type guard. Ela retorna true se o resource resolveu com sucesso um valor, permitindo que você acesse value() com segurança sem arriscar um erro em tempo de execução.

Com esses signals de estado, o template do componente pode ser transformado em um mapeamento limpo e declarativo de estado para UI. Usar o controle de fluxo nativo do Angular, particularmente @switch, é a maneira ideal de consumir o status do resource.

<input (input)="onSearch($event)" placeholder="Pesquisar produtos..." />

@switch (productsResource.status()) { @case ('loading') {
<p>Carregando...</p>
} @case ('resolved') {
<ul>
  @for (product of productsResource.value(); track product.id) {
  <li>{{ product.name }}</li>
  }
</ul>
} @case ('error') {
<p class="error-message">Erro: {{ productsResource.error()?.message }}</p>
} @default {
<p>Digite um termo de busca para encontrar produtos.</p>
} }

Este padrão de usar o signal status() com @switch é mais do que uma escolha estilística; ele cria uma UI mais robusta e manutenível. Ao usar um enum de status semelhante a uma máquina de estados, ele força o desenvolvedor a definir explicitamente a UI para cada estado distinto, eliminando ambiguidade e tornando impossível renderizar uma combinação inválida de elementos de UI.

Superpoderes Integrados: O Que o resource Cuida Para Você

O verdadeiro poder da Resource API não está apenas em simplificar o gerenciamento de estado, mas nos comportamentos robustos e integrados que ela lida automaticamente. Essas funcionalidades resolvem problemas comuns e difíceis em programação assíncrona.

Reatividade Automática: O loop reativo central é simples e poderoso: uma mudança em um signal de origem (como nosso searchQuery) dispara a função params para recomputar, que por sua vez dispara o loader para executar. Isso atualiza os signals de estado do resource, que finalmente causa a UI a atualizar declarativamente.

Imunidade a Race Conditions: Uma race condition ocorre quando múltiplas operações assíncronas são iniciadas em rápida sucessão e elas resolvem fora de ordem. A Resource API previne completamente este problema implementando semânticas integradas similares ao switchMap. Se uma nova requisição é disparada enquanto uma anterior ainda está em andamento, o resultado da requisição anterior, agora desatualizada, é simplesmente ignorado.

Cancelamento Gracioso: A função loader recebe um AbortSignal como parte de seu objeto ResourceLoaderParams. Este signal pode ser passado diretamente para a função fetch. Quando o resource decide que uma requisição pendente está obsoleta, ele usará este signal para abortar a requisição de rede subjacente.

Reload com Proteção: O objeto resource fornece um método reload() para cenários onde você precisa re-buscar dados usando exatamente os mesmos parâmetros. Este método vem com semânticas similares ao exhaustMap. Se reload() é chamado enquanto uma requisição já está em progresso, a nova chamada é simplesmente ignorada, prevenindo requisições duplicadas.

A Conveniência Definitiva: Simplificando Requisições HTTP com httpResource

Enquanto a primitiva genérica resource é poderosa e flexível, a vasta maioria das operações assíncronas em aplicações Angular são requisições HTTP. Reconhecendo isso, o time do Angular forneceu um wrapper especializado e de alto nível: httpResource.

Disponível em @angular/common/http, a função httpResource é construída diretamente sobre a primitiva resource. Ela usa HttpClient como seu loader por baixo dos panos, o que significa que ela se integra perfeitamente com os HttpInterceptors existentes da sua aplicação e utilitários de teste como HttpTestingController.

import { Component, signal, input } from '@angular/core';
import { httpResource } from '@angular/common/http'; // API Experimental
import { Product } from './product.model';

@Component({
  selector: 'app-user-details',
  //... component setup
})
export class UserDetailsComponent {
  // Usar um signal input para o ID do usuário da rota
  userId = input.required<string>();

  // A versão final e mais simples usando httpResource
  userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
}

A existência do httpResource é um movimento estratégico para guiar os desenvolvedores para um "poço de sucesso". Ao tornar o caminho mais fácil também a melhor prática para o cenário mais comum, o framework encoraja a adoção desse padrão limpo e padronizado.

Para controle mais avançado, você pode passar uma função reativa que retorna um objeto HttpResourceRequest completo, permitindo que você especifique métodos personalizados, headers, parâmetros de query e mais. Ele também fornece variantes para lidar com diferentes tipos de dados, como httpResource.text(), httpResource.blob() e httpResource.arrayBuffer().

Uma das funcionalidades mais poderosas é sua integração com bibliotecas de validação em tempo de execução. O objeto de opções do httpResource aceita uma função parse, que recebe a resposta bruta da API. Este é o lugar perfeito para usar uma biblioteca como Zod para validar os dados contra um schema e garantir type safety na fronteira da sua aplicação.

É importante notar que o design da Resource API é fundamentalmente sobre buscar dados (reads) e deliberadamente desencoraja seu uso para mutations (writes). Isso implicitamente encoraja uma separação limpa de responsabilidades, similar ao CQRS (Command Query Responsibility Segregation), onde você usa resources reativos para queries e chamadas padrão e imperativas do HttpClient dentro de métodos de serviço para commands.

Conclusão: Abraçando o Futuro dos Dados Reativos no Angular

A jornada do gerenciamento de estado verboso e manual do passado para o padrão limpo, declarativo e robusto habilitado pela Resource API é transformadora. Os benefícios chave são claros e convincentes:

  • Gerenciamento de estado simplificado com menos variáveis
  • Redução dramática no código boilerplate
  • Legibilidade e manutenibilidade vastamente melhoradas
  • Robustez integrada, lidando automaticamente com questões complexas como race conditions e cancelamento de requisições

A significância arquitetural dessa nova API se estende além de componentes individuais. Ao fornecer uma primitiva padronizada e declarativa para dependências de dados, a Resource API dá ao framework Angular visibilidade sem precedentes no fluxo de dados de uma aplicação. Isso abre as portas para otimizações poderosas em nível de framework no futuro.

A partir do Angular 20, a Resource API permanece experimental, e sua forma final pode evoluir baseada no feedback da comunidade. No entanto, ela representa a direção futura clara e empolgante para busca de dados em aplicações Angular modernas baseadas em signals. Desenvolvedores são encorajados a experimentar com resource e httpResource em seus projetos para experimentar em primeira mão os benefícios dessa nova e mais poderosa abordagem para gerenciar estado assíncrono.