Publicado em

- 10 minutos de leitura

Router Resources no Angular 22.2: guia prático

img of Router Resources no Angular 22.2: guia prático

O Angular 22.2 adicionou Router Resources ao Angular Router. A nova API permite declarar o carregamento de dados na própria configuração da rota usando resource(), httpResource(), rxResource() ou uma implementação customizada.

O guia mostra como configurar Router Resources e aplicar o recurso em páginas de detalhes, filtros por query param, rotas aninhadas, recarga manual e redirecionamento. Os exemplos também mostram quando bloquear a navegação e quando entregar o estado de carregamento ao componente.

O que são Router Resources

Router Resources conectam o ciclo de vida do Angular Router à Resource API. Cada rota pode declarar uma função resources que recebe o contexto da navegação e retorna um objeto com os recursos usados pela página.

O contexto expõe quatro signals:

  • params, com os parâmetros do caminho.
  • queryParams, com os parâmetros da query string.
  • fragment, com o fragmento da URL.
  • data, com os dados disponíveis no contexto da rota.

Quando um signal lido pelo recurso muda, o loader pode executar novamente. Por exemplo, uma navegação de /products/10 para /products/20 atualiza params e carrega o novo produto mesmo quando o Angular reutiliza o componente.

A função resources também roda em um contexto de injeção. Isso permite usar inject() para acessar clientes HTTP, serviços e stores dentro da configuração da rota.

Router Resources chegaram ao Angular 22.2 como Developer Preview. A API é pública, mas ainda pode receber ajustes antes de se tornar estável.

Como habilitar Router Resources

Adicione withRouterResources() ao provideRouter(). Use também withComponentInputBinding() para vincular os dados carregados aos inputs do componente.

Como os primeiros exemplos usam httpResource(), a configuração também precisa de provideHttpClient():

   // app.config.ts
import { provideHttpClient } from '@angular/common/http'
import { ApplicationConfig } from '@angular/core'
import { provideRouter, withComponentInputBinding, withRouterResources } from '@angular/router'
import { routes } from './app.routes'

export const appConfig: ApplicationConfig = {
	providers: [
		provideHttpClient(),
		provideRouter(routes, withComponentInputBinding(), withRouterResources())
	]
}

O nome de cada propriedade retornada por resources deve corresponder ao input que receberá o valor. Se a rota retorna { product: ... }, o componente precisa declarar um input chamado product.

Caso de uso 1: carregar um registro antes de abrir a página

Uma tela de edição costuma precisar do registro completo antes de montar o formulário. Nesse caso, use um Router Resource bloqueante, que é o comportamento padrão.

   // product.routes.ts
import { httpResource } from '@angular/common/http'
import { Routes } from '@angular/router'
import { ProductEditPage } from './product-edit.page'

export interface Product {
	id: string
	name: string
	price: number
}

export const productRoutes: Routes = [
	{
		path: 'products/:id/edit',
		component: ProductEditPage,
		resources: (context) => ({
			product: httpResource<Product>(() => `/api/products/${context.params()['id']}`)
		})
	}
]

O Router aguarda o carregamento terminar e entrega apenas o valor ao componente. Por isso, o input recebe Product, e não Resource<Product>:

   // product-edit.page.ts
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { Product } from './product.routes'

@Component({
	selector: 'app-product-edit-page',
	changeDetection: ChangeDetectionStrategy.OnPush,
	template: `
		<h1>Editar {{ product().name }}</h1>
		<p>Preço atual: {{ product().price }}</p>
	`
})
export class ProductEditPage {
	readonly product = input.required<Product>()
}

O componente não precisa representar o estado de carregamento porque ele só é ativado quando o recurso termina. Se o carregamento falhar, a navegação é cancelada e o Router emite um NavigationError.

Mesmo quando o recurso possui defaultValue, o Angular 22.2 verifica isLoading() para decidir se deve concluir a navegação. Ter um valor inicial não transforma o recurso em não bloqueante.

Caso de uso 2: exibir skeleton com nonBlocking()

Um relatório pesado pode levar alguns segundos para ficar pronto. Mostrar a estrutura da página imediatamente costuma produzir uma experiência melhor do que manter o usuário na tela anterior.

Envolva o recurso com nonBlocking() para concluir a navegação sem esperar pelos dados:

   // report.routes.ts
import { httpResource } from '@angular/common/http'
import { Routes, nonBlocking } from '@angular/router'
import { ReportPage } from './report.page'

export interface Report {
	title: string
	total: number
}

export const reportRoutes: Routes = [
	{
		path: 'reports/:id',
		component: ReportPage,
		resources: (context) => ({
			report: nonBlocking(httpResource<Report>(() => `/api/reports/${context.params()['id']}`))
		})
	}
]

No modo não bloqueante, o componente recebe o objeto Resource completo. O template pode ler isLoading(), error(), hasValue() e value():

   // report.page.ts
import { ChangeDetectionStrategy, Component, input, Resource } from '@angular/core'
import { Report } from './report.routes'

@Component({
	selector: 'app-report-page',
	changeDetection: ChangeDetectionStrategy.OnPush,
	template: `
		@let currentReport = report();

		@if (currentReport.isLoading()) {
			<section aria-busy="true">
				<p>Carregando relatório...</p>
			</section>
		} @else if (currentReport.error()) {
			<p role="alert">Não foi possível carregar o relatório.</p>
		} @else if (currentReport.hasValue()) {
			<h1>{{ currentReport.value().title }}</h1>
			<p>Total: {{ currentReport.value().total }}</p>
		}
	`
})
export class ReportPage {
	readonly report = input.required<Resource<Report>>()
}

Verifique error() ou hasValue() antes de ler value(). Quando o loader falha e não existe um valor disponível, a leitura de value() lança o erro.

Imagem com o texto: Curso Angular Moderno e um homem de pele parda usando óculos com a logo do Angular atrás. Logo abaixo existe um botão com o texto "Eu quero!"

Caso de uso 3: filtrar dados por query param

O ResourceContext facilita a criação de listagens reativas. Neste exemplo, somente o parâmetro category participa da requisição:

   // catalog.routes.ts
import { httpResource } from '@angular/common/http'
import { Routes, nonBlocking } from '@angular/router'
import { CatalogPage } from './catalog.page'

interface ProductSummary {
	id: string
	name: string
}

export const catalogRoutes: Routes = [
	{
		path: 'catalog',
		component: CatalogPage,
		resources: (context) => ({
			products: nonBlocking(
				httpResource<ProductSummary[]>(() => ({
					url: '/api/products',
					params: {
						category: context.queryParams()['category'] ?? 'all'
					}
				}))
			)
		})
	}
]

A mudança de /catalog?category=books para /catalog?category=games refaz a chamada. Alterar um query param que não foi lido, como theme, não recarrega o recurso.

Evite retornar context.queryParams() inteiro no callback quando a requisição depende de uma única propriedade. O Router cria um novo objeto de parâmetros a cada navegação, o que pode provocar carregamentos desnecessários.

O componente pode atualizar a URL com queryParams e deixar o Router Resource cuidar da nova requisição:

   <nav aria-label="Categorias">
	<a routerLink="/catalog" [queryParams]="{ category: 'books' }">Livros</a>
	<a routerLink="/catalog" [queryParams]="{ category: 'games' }">Jogos</a>
</nav>

Caso de uso 4: carregar dados de rotas aninhadas em paralelo

Resolvers tradicionais executam em sequência do pai para o filho. Se o resolver do pai leva 400 ms e o do filho leva 600 ms, a navegação pode esperar cerca de 1 segundo.

Router Resources da hierarquia correspondente iniciam em paralelo. Com os mesmos tempos, a espera fica próxima dos 600 ms do recurso mais lento.

   export const accountRoutes: Routes = [
	{
		path: 'accounts/:accountId',
		component: AccountLayout,
		resources: (context) => ({
			account: httpResource<Account>(() => `/api/accounts/${context.params()['accountId']}`)
		}),
		children: [
			{
				path: 'invoices',
				component: InvoiceListPage,
				resources: (context) => ({
					invoices: httpResource<Invoice[]>(
						() => `/api/accounts/${context.params()['accountId']}/invoices`
					)
				})
			}
		]
	}
]

Esse comportamento ajuda em layouts com dados próprios e páginas filhas que carregam informações independentes. Se o recurso filho depende do resultado do pai para montar a requisição, a dependência ainda precisa aparecer no seu modelo de dados.

Caso de uso 5: aproveitar um serviço baseado em Observable

Projetos que já usam HttpClient em serviços podem adotar rxResource() sem converter manualmente cada Observable em Promise. O callback stream recebe o parâmetro reativo e retorna o Observable existente:

   import { inject } from '@angular/core'
import { rxResource } from '@angular/core/rxjs-interop'
import { Routes } from '@angular/router'
import { OrderApi } from './order-api.service'
import { OrderPage } from './order.page'

export const orderRoutes: Routes = [
	{
		path: 'orders/:id',
		component: OrderPage,
		resources: (context) => {
			const orderApi = inject(OrderApi)

			return {
				order: rxResource({
					params: () => context.params()['id'],
					stream: ({ params: id }) => orderApi.findById(id)
				})
			}
		}
	}
]

Use loader com resource() e stream com rxResource(). Nos dois casos, o Router observa o estado do recurso e aplica a mesma regra de bloqueio.

Caso de uso 6: redirecionar quando o registro não existe

Um Router Resource bloqueante pode lançar RedirectCommand. O Router cancela a navegação atual e abre a URL indicada.

O exemplo abaixo usa um cliente que retorna Product | null e aceita o AbortSignal da Resource API:

   import { inject, resource } from '@angular/core'
import { RedirectCommand, Router, Routes } from '@angular/router'
import { ProductApi } from './product-api.service'
import { ProductPage } from './product.page'

export const routes: Routes = [
	{
		path: 'products/:id',
		component: ProductPage,
		resources: (context) => {
			const productApi = inject(ProductApi)
			const router = inject(Router)

			return {
				product: resource({
					params: () => context.params()['id'],
					loader: async ({ params: id, abortSignal }) => {
						const product = await productApi.findById(id, abortSignal)

						if (!product) {
							throw new RedirectCommand(router.parseUrl('/products/not-found'))
						}

						return product
					}
				})
			}
		}
	}
]

Repassar abortSignal permite cancelar uma chamada pendente quando outra navegação substitui a atual. O método ProductApi.findById pode encaminhar esse signal para fetch ou para outro cliente que aceite cancelamento.

O redirecionamento dentro do loader faz sentido para recursos bloqueantes porque o Router ainda não concluiu a navegação. Em um recurso não bloqueante, a página já foi ativada e o erro deve ser tratado pelo componente.

Caso de uso 7: recarregar sem navegar outra vez

Resolvers precisam de uma nova navegação para executar novamente. Um Router Resource pode ser recarregado de forma isolada com reload().

Recursos bloqueantes entregam somente o valor ao input. Para acessar a instância do recurso, use o mapa resources do ActivatedRoute:

   import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Product } from './product.routes'

@Component({
	selector: 'app-product-edit-page',
	changeDetection: ChangeDetectionStrategy.OnPush,
	template: `
		<h1>{{ product().name }}</h1>
		<button type="button" (click)="reload()">Atualizar dados</button>
	`
})
export class ProductEditPage {
	readonly product = input.required<Product>()

	private readonly productResource = inject(ActivatedRoute).resources?.['product']

	reload(): void {
		this.productResource?.reload()
	}
}

O Router atualiza o input quando a nova carga termina. Guards não executam novamente e a rota não precisa ser remontada.

Os recursos expostos por ActivatedRoute são somente leitura. Você pode consultar seus signals e chamar reload(), mas não pode usar set() ou update().

Durante uma navegação pendente, o Router preserva o valor atual até o próximo recurso resolver. Ao navegar de /products/10 para /products/20 com o mesmo componente, a tela continua mostrando o produto 10 e troca diretamente para o produto 20 quando a nova carga termina.

Quando usar bloqueante ou não bloqueante

A escolha depende do quanto a tela consegue funcionar sem o dado.

SituaçãoEstratégia sugerida
Formulário que precisa do registro para ser criadoBloqueante
Página em que um dado ausente exige redirecionamentoBloqueante
Dashboard com skeletons independentesnonBlocking()
Lista que pode mostrar carregamento durante filtrosnonBlocking()
Dado pequeno e essencial para todo o layoutBloqueante
Conteúdo secundário ou lentononBlocking()

Também é possível misturar os dois modos na mesma rota. O Router espera pelos recursos bloqueantes e entrega os não bloqueantes ainda com seu ciclo de carregamento.

   resources: (context) => ({
	product: httpResource<Product>(() => `/api/products/${context.params()['id']}`),
	recommendations: nonBlocking(
		httpResource<Product[]>(() => `/api/products/${context.params()['id']}/recommendations`)
	)
})

Nesse exemplo, o produto é necessário para abrir a página. As recomendações podem aparecer depois sem atrasar a navegação.

Router Resources ou resolvers

Resolvers continuam disponíveis e são uma escolha segura para projetos que não adotam APIs em Developer Preview. Router Resources atendem melhor fluxos que precisam reagir a signals, recarregar dados isoladamente ou combinar carregamento bloqueante e não bloqueante.

CaracterísticaResolverRouter Resource
Valor retornadoValor, Promise ou ObservableResource
Execução na hierarquiaSequencialParalela
Navegação bloqueanteSimSim, por padrão
Navegação não bloqueanteExige outra abordagemnonBlocking()
Atualização por signalsManualIntegrada
Reload sem nova navegaçãoNãoSim
Estabilidade no Angular 22.2EstávelDeveloper Preview

Ao usar withComponentInputBinding(), lembre que o Router pode preencher inputs a partir de diferentes fontes. Em caso de nomes iguais, Router Resources têm prioridade sobre data e resolvers, seguidos pelos parâmetros do caminho e pelos query params.

Cuidados ao usar Router Resources

Leia apenas os parâmetros necessários dentro de params e queryParams. Isso evita requisições causadas por mudanças que não afetam o dado.

Encaminhe o abortSignal quando o loader usa uma API compatível com cancelamento. Uma navegação pode ser substituída rapidamente, e manter a chamada anterior ativa desperdiça recursos.

Trate de forma diferente os erros de recursos bloqueantes e não bloqueantes. No primeiro caso, o Router cancela a navegação. No segundo, o componente recebe o erro pelo signal error().

Mantenha regras de negócio e acesso a dados em serviços. A rota deve coordenar parâmetros, carregamento e redirecionamento, enquanto o cliente ou serviço continua responsável pela comunicação com a API.

Conclusão

Router Resources colocam o carregamento de dados da rota no modelo reativo do Angular. A integração com signals permite responder a mudanças de parâmetros, executar recursos em paralelo e atualizar um dado sem repetir toda a navegação.

Comece por uma rota de detalhes e escolha o modo de carregamento conforme a experiência desejada. Use recursos bloqueantes para dados essenciais e nonBlocking() quando a tela puder representar carregamento, sucesso e erro por conta própria.

Referências

Entre na nossa comunidade!

Receba novos posts, novidades do ecossistema Angular e muito mais.

Sobre o autor

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