Router Resources, released in Angular v22.2, is the long-awaited full-scale integration API for the Router and Signals. In this article, I will explain its basic usage through specific use cases.

How to Use Router Resources

Blocking Data Fetching During Navigation

The most typical use case is fetching data during a Router navigation and waiting for the data to be ready before moving to the page. Until now, Observable/Promise-based data resolvers handled this, but an intuitive Signal-based API has been introduced to take over the same role. Let’s look at a concrete comparison between the old and new code.

Take the case of fetching user data using an ID in the URL path during page navigation. Conventionally, you would declare a resolver function like this, and the component would receive the already fetched user data.

// app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [provideRouter(routes, withComponentInputBinding())], }; // app.routes.ts import { ResolveFn, Routes } from '@angular/router'; import { UserProfile } from './user-profile'; import { User, UserService } from './user.service'; const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); const userId = route.paramMap.get('id')!; return userService.getUser(userId); }; export const routes: Routes = [ { path: 'users/:id', component: UserProfile, resolve: { user: userResolver }, }, ]; 

The component receives the fetched User as an input signal via withComponentInputBinding. This is typical data fetching during navigation using a data resolver.

// user-profile.ts import { Component, input } from '@angular/core'; import { User } from './user.service'; @Component({ selector: 'app-user-profile', template: ` <h2>{{ user().name }}</h2> `, }) export class UserProfile { readonly user = input.required<User>(); } 

Rewriting this with Router Resources looks like the following. First, to use Router Resources, add withRouterResources() to the Router configuration. Then, declare a function in the Route.resources property instead of a data resolver. This function takes a ResourceContext object as an argument and returns a key-value object of resources. ResourceContext holds various parameters as Signals; for example, ctx.params() is a Signal containing the path parameters. Since the resolved value() of the resource is passed to the component, the code remains exactly the same as in the data resolver case and requires no changes.

// app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideRouter, withComponentInputBinding, withRouterResources, } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideRouter( routes, withComponentInputBinding(), withRouterResources(), // Added ), ], }; // app.routes.ts import { inject, resource } from '@angular/core'; import { Routes } from '@angular/router'; import { UserProfile } from './user-profile'; import { UserService } from './user.service'; export const routes: Routes = [ { path: 'users/:id', component: UserProfile, resources: (ctx) => { const userService = inject(UserService); return { user: resource({ params: () => ctx.params()['id'] as string, loader: ({ params: id }) => userService.getUser(id), }), }; }, }, ]; 

In this example, the data resolver was replaced by Router Resources, but if it were just about replacement, a new API wouldn’t be necessary. From here, let’s look at the advantages that only Router Resources can offer.

Non-blocking Data Fetching

The behavior of a data resolver is blocking , meaning it navigates to the page only after the data fetch is complete. While this helps in ensuring state consistency before reaching a page that needs data, it has several drawbacks. One is a user experience issue where data fetching must be waited for on the previous page, making it feel as though the screen has frozen. It is difficult to manage waiting time with skeletons or loading UIs. Another issue is the fragmentation of code. Since data resolvers are always blocking, fetching non-blocking data ends up being written on the component side. Even though it is the same concern—data fetching associated with page navigation—the code gets scattered.

Router Resources allows you to choose whether data fetching should be blocking or non-blocking. This allows data fetching code associated with Router-dependent page transitions to be centralized in one place. Let’s look at it concretely.

Unlike the previous example, let’s display a loading indicator while fetching user data, an error message if it fails, and the user info once fetched. With the conventional API, the component would receive the path parameter as an input signal via withComponentInputBinding, and it would also be the component’s responsibility to create a resource using that parameter.

// user-profile.ts import { Component, inject, resource } from '@angular/core'; import { UserService } from './user.service'; @Component({ template: ` @if (userResource.isLoading()) { <p>Loading...</p> } @else if (userResource.error()) { <p>Failed to load user.</p> } @else if (userResource.value(); as user) { <h2>{{ user.name }}</h2> <p>ID: {{ user.id }}</p> } `, }) export class UserProfile { private readonly userService = inject(UserService); private readonly id = input.required<string>(); // users/:id readonly userResource = resource({ params: () => this.id(), loader: ({ params: id }) => this.userService.getUser(id), }); } // app.routes.ts import { Routes } from '@angular/router'; import { UserProfile } from './user-profile'; export const routes: Routes = [ { path: 'users/:id', component: UserProfile }, ]; 

With Router Resources, it looks like this. You create the resource in the resources function of the route configuration, but unlike the previous example, you wrap the resource with the nonBlocking function. By doing this, the component receives not the value() fetched by the resource, but the resource object itself. In other words, it receives not user but userResource.

// app.routes.ts import { inject, resource } from '@angular/core'; import { nonBlocking, Routes } from '@angular/router'; import { UserProfile } from './user-profile'; import { UserService } from './user.service'; export const routes: Routes = [ { path: 'users/:id', component: UserProfile, resources: (ctx) => { const userService = inject(UserService); return { userResource: nonBlocking( resource({ params: () => ctx.params()['id'] as string | undefined, loader: ({ params: id }) => userService.getUser(id), }), ), }; }, }, ]; 

The component receives the resource object as an input signal from the start. Since the resource object also holds the data fetching state and errors, it can handle UI changes according to the state, just like before. It’s also worth noting that the concern regarding data fetching has disappeared from the component, and the dependency on the service has been eliminated. Through Router Resources, a component can achieve a higher level of separation as a presentation layer. The internal details of how data is fetched are encapsulated behind the resource.

// user-profile.ts import { Component, input, Resource } from '@angular/core'; import { User } from './user.service'; @Component({ template: ` @let res = userResource(); @if (res.isLoading()) { <p>Loading...</p> } @else if (res.error()) { <p>Failed to load user.</p> } @else if (res.value(); as user) { <h2>{{ user.name }}</h2> <p>ID: {{ user.id }}</p> } `, }) export class UserProfile { readonly userResource = input.required<Resource<User | undefined>>(); } 

Parallel Execution of Data Fetching Across Parent and Child Routes

Another advantage of Router Resources is the ability to execute data fetching across multiple routes in parallel. Data resolvers are executed sequentially from the parent route to the child route. Even if the data fetches have no dependencies on each other, the child must wait for the parent to complete.

Let’s take a page that displays a user profile and a list of posts as an example. The parent route fetches the user information, and the child route fetches that user’s list of posts. Both can be fetched as long as the ID from the URL is known; there’s no need to wait for the user info fetch to complete to fetch the post list.

// app.routes.ts import { inject } from '@angular/core'; import { ResolveFn, Routes } from '@angular/router'; import { UserPage } from './user-page'; import { UserPosts } from './user-posts'; import { User, UserService } from './user.service'; import { Post, PostService } from './post.service'; const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); const userId = route.paramMap.get('id')!; return userService.getUser(userId); }; const postsResolver: ResolveFn<Post[]> = (route) => { const postService = inject(PostService); const userId = route.paramMap.get('id')!; return postService.getPosts(userId); }; export const routes: Routes = [ { path: 'users/:id', component: UserPage, resolve: { user: userResolver }, children: [ { path: '', component: UserPosts, resolve: { posts: postsResolver }, }, ], }, ]; 

UserPage displays the user information and a RouterOutlet, inside which UserPosts displays the list of posts. Since a child route with an empty path inherits the parent’s path parameters, the same id can be used in the child. As in the previous examples, the component receives the fetched user and posts via withComponentInputBinding. In this setup, if fetching user information takes 200ms and fetching the post list takes 300ms, the total wait time for data fetching would be 500ms.

Replacing this with Router Resources looks like the following. Router Resources loads resources for the parent and child routes that match the destination in parallel. Since the fetching of user info and the post list proceeds in parallel, in the previous example, the total wait time for data fetching would not be 500ms, but around 300ms, which is the duration of the slower one.

// app.routes.ts import { inject, resource } from '@angular/core'; import { ResourceContext, Routes } from '@angular/router'; import { UserPage } from './user-page'; import { UserPosts } from './user-posts'; import { UserService } from './user.service'; import { PostService } from './post.service'; const userResource = (ctx: ResourceContext) => { const userService = inject(UserService); return resource({ params: () => ctx.params()['id'] as string, loader: ({ params: id }) => userService.getUser(id), }); }; const postsResource = (ctx: ResourceContext) => { const postService = inject(PostService); return resource({ params: () => ctx.params()['id'] as string, loader: ({ params: id }) => postService.getPosts(id), }); }; export const routes: Routes = [ { path: 'users/:id', component: UserPage, resources: (ctx) => ({ user: userResource(ctx) }), children: [ { path: '', component: UserPosts, resources: (ctx) => ({ posts: postsResource(ctx) }), }, ], }, ]; 

Appendix: Depending on a Parent Route’s Resource

Consider a case where a child route fetches a post list using an organization ID included in the user information fetched by the parent route. In this case, the fetching criteria cannot be determined by URL parameters alone; the child depends on the parent’s resource. As of v22.2.0, Router Resources does not have an API to reference a parent route’s resource from the context of the resources function, but PR #70587, which addresses this use case, is currently in development.

In this draft, ResourceContext.resources is exposed to inherit resources from ancestor routes, allowing resources declared in the parent to be retrieved via ctx.resources(). Rewriting the previous postsResource according to this proposal would look like the following. In this example, assume User has an organizationId and the post fetching service accepts that ID.

// app.routes.ts import { inject, Resource, resource } from '@angular/core'; import { ResourceContext } from '@angular/router'; import { User } from './user.service'; import { PostService } from './post.service'; const postsResource = (ctx: ResourceContext) => { const postService = inject(PostService); return resource({ params: ({ chain }) => { const userResource = ctx.resources()['user'] as Resource<User | undefined>; return chain(userResource)?.organizationId; }, loader: ({ params: organizationId }) => postService.getPostsByOrganization(organizationId), }); }; 

Reloading Without Re-navigation

Consider a scenario where you edit a name in a user list and want to re-fetch the list after saving. Instead of just displaying the saved value, you want to reflect the updated data from the server in the list. In this example, assume UserService.getUsers() returns the list as a Promise<User[]> and saveUser(user) saves the user info and returns a Promise<void>.

If you are fetching the list using a data resolver, re-fetching after saving also requires navigation. You would specify onSameUrlNavigation: 'reload' to handle navigation to the same URL, and runGuardsAndResolvers: 'always' to re-execute the resolver even if the path parameters haven’t changed. With this method, route matching and guard evaluation are also re-executed just to update the list after saving.

// app.routes.ts import { inject } from '@angular/core'; import { ResolveFn, Routes } from '@angular/router'; import { UserList } from './user-list'; import { User, UserService } from './user.service'; const usersResolver: ResolveFn<User[]> = () => { const userService = inject(UserService); return userService.getUsers(); }; export const routes: Routes = [ { path: 'users', component: UserList, resolve: { users: usersResolver }, runGuardsAndResolvers: 'always', }, ]; // user-list.ts import { Component, inject, input } from '@angular/core'; import { Router } from '@angular/router'; import { User, UserService } from './user.service'; @Component({ template: ` @for (user of users(); track user.id) { <input #name [value]="user.name" /> <button (click)="saveUser(user, name.value)">Save</button> } `, }) export class UserList { readonly users = input.required<User[]>(); private readonly userService = inject(UserService); private readonly router = inject(Router); async saveUser(user: User, name: string) { await this.userService.saveUser({ ...user, name }); await this.router.navigateByUrl(this.router.url, { onSameUrlNavigation: 'reload', }); } } 

With Router Resources, you only need to call reload() on the list’s resource after the save is complete.

Related posts