Publicado em

- 6 minutos de leitura

Angular 19: Conheça a nova Resource API

img of Angular 19: Conheça a nova Resource API

Este artigo é baseado no excelente trabalho de Enea Jahollari

Angular 19 vai introduzir uma nova funcionalidade chamada Resource API para carregar recursos. Isso permitirá recuperar dados de um servidor HTTP remoto, monitorar o status da requisição e atualizar os dados localmente quando necessário.

Caso você goste mais do formato em vídeo, temos o seguinte conteúdo no nosso canal no Youtube:

Carregando Dados com a Resource API

A API de recursos é bem simples em sua essência. Vamos ver o exemplo mais básico de como utilizá-la.

   import { resource } from '@angular/core'

@Component({})
export class MeuComponente {
	recursoTodo = resource({
		loader: () => {
			return Promise.resolve({ id: 1, title: 'Hello World', completed: false })
		}
	})

	constructor() {
		effect(() => {
			console.log('Valor: ', this.recursoTodo.value())
			console.log('Status: ', this.recursoTodo.status())
			console.log('Erro: ', this.recursoTodo.error())
		})
	}
}

A API de recursos usa Promises no parâmetro loader por padrão. Além disso, a API de recursos retorna um objeto WritableResource, que nos permite atualizar os dados localmente quando necessário.

Podemos acessar o valor atual do recurso usando o signal value, o status do recurso com o signal status, e o erro do recurso através do signal error.

O código acima vai imprimir o seguinte:

   Valor: undefined
Status: 2 (loading)
Erro: undefined

Valor: { id: 1, title: "Hello World", completed: false }
Status: 4 (resolved)
Erro: undefined

Como Atualizar os Dados Localmente

Agora, vamos ver como atualizar os dados localmente.

   import { resource } from '@angular/core'

@Component({
	template: ` <button (click)="atualizarTodo()">Atualizar</button> `
})
export class MeuComponente {
	recursoTodo = resource({
		loader: () => {
			return Promise.resolve({ id: 1, title: 'Hello World', completed: false })
		}
	})

	atualizarTodo() {
		this.recursoTodo.update((valor) => {
			if (!valor) return undefined

			return { ...valor, title: 'atualizado' }
		})
	}
}

Podemos atualizar os dados localmente usando o método update (que utiliza value.update por baixo dos panos).

Isso vai resultar em:

   Valor: { id: 1, title: "atualizado", completed: false }
Status: 5 (local)
Erro: undefined

O status 'local' significa que os dados foram atualizados localmente.

Carregando os dados

Agora, vamos fazer uma requisição adequada para o servidor. Vamos carregar alguns todos da API JSONPlaceholder.

   interface Todo {
	id: number
	title: string
	completed: boolean
}

@Component()
export class MeuComponente {
	recursoTodos = resource({
		loader: () => {
			return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`).then(
				(res) => res.json() as Promise<Todo[]>
			)
		}
	})
}

O recursoTodos vai começar a fazer a requisição para o servidor assim que for criado.

Claro, o recursoTodos não terá um valor ainda, porque a requisição ainda está em andamento.

O código acima vai imprimir o seguinte:

   Valor: undefined
Status: 2 (loading)
Erro: undefined

Valor: [{ id: 1, title: "Hello World", completed: false }, { id: 2, title: "Hello World", completed: false }, ...]
Status: 4 (resolved)
Erro: undefined

Atualizando os dados

Vamos supor que queremos atualizar os dados quando o usuário clicar em um botão.

   import { resource } from '@angular/core'

@Component()
export class MeuComponente {
	recursoTodos = resource({
		loader: () => {
			return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`).then(
				(res) => res.json() as Promise<Todo[]>
			)
		}
	})

	atualizar() {
		this.recursoTodos.reload()
	}
}

A função reload vai rodar a função loader novamente. Se você chamar a função reload várias vezes, a função loader será chamada apenas uma vez até que a requisição anterior seja finalizada (comportamento semelhante ao exhaustMap no RxJS).

Carregando dados específicos com base em outros signals

Vamos supor que queremos carregar os todos com base no signal todoId.

   import { resource } from '@angular/core'

@Component()
export class MeuComponente {
	todoId = signal(1)

	recursoTodo = resource({
		loader: () => {
			return fetch(`https://jsonplaceholder.typicode.com/todos/${this.todoId()}`).then(
				(res) => res.json() as Promise<Todo>
			)
		}
	})
}

Isso vai funcionar bem, mas vale notar que o loader não é rastreado, ou seja, se o signal todoId mudar, a requisição não será chamada novamente. Vamos torná-lo mais reativo!

Imagem com o texto: Curso testes automatizado com Angular e um Robo azul com uma logo do Angular atrás. Logo abaixo existe um botão com o texto 'Eu quero!'

Separando a requisição e o loader

Agora, queremos que o recurso atualize os dados (chamando o loader novamente) sempre que o todoId mudar. Para isso, podemos usar o campo request do recurso. Podemos passar um signal para ele, e o recurso será rastreado.

   recursoTodo = resource({
	request: this.todoId,
	loader: ({ request: todoId }) => {
		return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`).then(
			(res) => res.json() as Promise<Todo>
		)
	}
})

Agora, quando o signal todoId mudar, a API de recursos vai recuperar os novos dados automaticamente.

E o que acontece se tivermos requisições anteriores não finalizadas? Podemos cancelar a requisição anterior quando o todoId mudar, utilizando o abortSignal passado para a função loader.

   recursoTodo = resource({
	request: this.todoId,
	loader: ({ request: todoId, abortSignal }) => {
		return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
			signal: abortSignal
		}).then((res) => res.json() as Promise<Todo>)
	}
})

Isso vai cancelar a requisição anterior quando o todoId mudar, caso a requisição anterior ainda esteja em andamento.

Também podemos ter múltiplas dependências de requisição, por exemplo:

   limite = signal(10)
consulta = signal('')

recursoTodos = resource({
	request: () => ({
		limite: this.limite(),
		consulta: this.consulta()
	}),
	loader: ({ request, abortSignal }) => {
		const { limite, consulta } = request as { limite: number; consulta: string }
		return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=${limite}&query=${consulta}`, {
			signal: abortSignal
		}).then((res) => res.json() as Promise<Todo[]>)
	}
})

Agora, o recursoTodos vai fazer a requisição com base nos signals limite e consulta. Sempre que algum desses signals mudar, a função loader será chamada novamente.

O que acontece quando temos uma requisição em andamento e atualizamos os dados localmente?

Se isso acontecer, a API de recursos vai atualizar os dados localmente, mas cancelar a requisição em andamento.

Criando recursos mais reutilizáveis

Ao separar os valores reativos da função loader, podemos mover a lógica do loader para uma função separada, e armazená-la onde quisermos.

Antes:

   recursoTodo = resource({
	request: this.todoId,
	loader: ({ request: todoId, abortSignal }) => {
		return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
			signal: abortSignal
		}).then((res) => res.json() as Promise<Todo>)
	}
})

Depois:

   import { ResourceLoaderParams } from '@angular/core'

function carregarTodo({
	request: todoId,
	abortSignal
}: ResourceLoaderParams<number>): Promise<Todo> {
	return fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
		signal: abortSignal
	}).then((res) => res.json() as Promise<Todo>)
}

recursoTodo = resource({ request: this.todoId, loader: carregarTodo })

A função carregarTodo pode ser movida para qualquer lugar e reutilizada por outros recursos. O tipo ResourceLoaderParams contém todas as informações necessárias para fazer a requisição, e o parâmetro request é o que é passado para a função loader.

RxResource: A API baseada em Observables

O Angular sempre usou Observables quando se trata de carregamento de dados. Isso significa que podemos usar Observables para derivar o carregamento de dados, em vez de usar signals e promises.

Para tornar isso possível, podemos usar a função rxResource.

   import { rxResource } from '@angular/core/rxjs-interop'

@Component()
export class MeuComponente {
	limite = signal(10)

	recursoTodos = rxResource({
		request: this.limite,
		loader: (limite) => {
			return this.http.get<Todo[]>(`https://jsonplaceholder.typicode.com/todos?_limit=${limite}`)
		}
	})
}

Isso fará a requisição com base no signal limite, e a função loader poderá usar o valor de limite para fazer a requisição. Sempre que o signal limite mudar, a função loader será chamada novamente, e a requisição anterior será cancelada (comportamento semelhante ao switchMap no RxJS).

Assim como podemos alterar o estado local com signals, podemos também modificá-lo usando Observables na função rxResource.

Resumo

Agora temos 2 novas primitivas [resource, rxResource] que vão facilitar nossa vida ao lidar com o carregamento de dados no Angular. Essas primitivas eram muito aguardadas e serão lançadas na versão 19 como pré-visualizações para desenvolvedores.

Link do PR: https://github.com/angular/angular/pull/58255

Sobre o autor

Author's photo
Henrique Custódia Arquiteto Frontend, entusiasta Angular, cat lover, criador de conteúdo e fundador da Code Dimension!

Assine nossa Newsletter e receba os últimos posts e novidades da Code Dimension!