docs(router): Add router resource docs

Adds doc (but no navigation entry) for router resources. Feature not released yet
but getting documentation in place.
This commit is contained in:
Andrew Scott
2026-08-27 12:45:27 -07:00
committed by GitHub
parent 4fc45a9b38
commit 6f1dc268cc
2 changed files with 241 additions and 0 deletions
@@ -0,0 +1,217 @@
# Data fetching with resources
The Angular Router integrates with Angular Signals through the `resources` route configuration. This allows you to fetch data reactively using `Resource` APIs.
## Setup
To enable this feature, provide `withRouterResources()` to your router configuration:
```ts
import {provideRouter, withComponentInputBinding, withRouterResources} from '@angular/router';
bootstrapApplication(App, {
providers: [provideRouter(routes, withComponentInputBinding(), withRouterResources())],
});
```
You can then define resources in your `Route` definitions and access them directly as component inputs.
The `resources` function runs in an injection context, allowing you to use `inject()` to access services, API clients, or stores directly inside the route definition.
```angular-ts
import {Component, inject, input, resource} from '@angular/core';
import {Routes} from '@angular/router';
import {UserService} from './user.service';
const routes: Routes = [
{
path: 'user/:id',
component: UserProfile,
resources: (ctx) => {
const userService = inject(UserService);
return {
user: resource({
params: () => ctx.params()['id'],
loader: ({params: id}) => userService.getUser(id),
}),
};
},
},
];
@Component({
template: `<p>User: {{ user().name }}</p>`,
})
export class UserProfile {
// The router automatically binds only the value for blocking resources.
user = input.required<User>();
}
```
TIP: Notice we map the exact primitive ID we need in `params: () => ctx.params()['id']`. Passing the entire parameters object (e.g. `params: () => ctx.params()`) can cause unnecessary resource reloads during navigations. Because the router generates a new object identity for the parameters on navigation, the resource will trigger a refetch even if the specific `id` value you care about hasn't changed.
### ResourceContext
The `resources` function receives a `ResourceContext` providing access to route signals (such as `params`, `queryParams`, and `data`) as well as the static `snapshot`.
NOTE: Route resources execute in parallel and cannot access or depend on other resources defined on the route. If you need dependent data fetching, manage that sequence within a single resource's loader.
### Resource implementations and async configuration
The `resources` map supports any Angular `Resource` implementation (such as `resource()`, `rxResource()`, or custom resources).
```ts
import {Routes} from '@angular/router';
import {rxResource} from '@angular/core/rxjs-interop';
const routes: Routes = [
{
path: 'user/:id',
component: UserProfile,
resources: (ctx) => ({
user: rxResource({
params: () => ctx.params()['id'],
stream: ({params: id}) => fetchUserObservable(id),
}),
}),
},
];
```
NOTE: `rxResource` uses the `stream` property instead of `loader` to accept a function that returns an Observable.
The `resources` function can also be `async` and return a `Promise` if you need to perform asynchronous setup or dynamic imports before configuring resources:
```ts
resources: async (ctx) => {
const {fetchUserData} = await import('./user-api');
return {
user: resource({
params: () => ctx.params()['id'],
loader: ({params: id}) => fetchUserData(id),
}),
};
},
```
## Accessing resources via ActivatedRoute
When using `withComponentInputBinding()`, blocking resources bind only their unwrapped value directly to component inputs. If you need to interact with the underlying `Resource` instance (for example, to trigger a manual `.reload()` or inspect status signals), you can access it through `ActivatedRoute` or `ActivatedRouteSnapshot`.
```angular-ts
import {Component, inject, input} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
@Component({
template: `
<p>User: {{ user().name }}</p>
<button (click)="reload()">Refresh</button>
`,
})
export class UserProfile {
user = input.required<User>();
private userResource = inject(ActivatedRoute).resources?.['user'];
reload() {
this.userResource?.reload();
}
}
```
## Blocking and non-blocking resources
By default, all resources returned from `resources` are **blocking**. The router waits until the data is fully loaded before activating the route and component.
**For blocking resources, the router binds only the resolved value to the component input.** The input type in your component is `T` instead of `Resource<T>`.
This simplifies your component because it does not need to handle loading or error states. Because the router blocks navigation until the resource is loaded, the component never observes a `loading` state. If the resource throws an error, the router cancels the navigation, so the component never observes an `error` state.
If you prefer to handle loading states in the UI, use the `nonBlocking()` wrapper utility. Non-blocking resources do not halt navigation. The router activates the component immediately, allowing the UI to handle loading or skeleton states.
**For non-blocking resources, the router binds the full `Resource<T>` object to the component input.** This allows you to access `.isLoading()`, `.error()`, and other resource signals in your component.
```angular-ts
import {Component, input, Resource, resource} from '@angular/core';
import {Routes, nonBlocking} from '@angular/router';
const routes: Routes = [
{
path: 'reports',
component: Reports,
resources: (ctx) => ({
reportData: nonBlocking(
resource({
loader: () => fetchHeavyReportData(),
}),
),
}),
},
];
@Component({
template: `
@if (reportData().isLoading()) {
<p>Loading...</p>
} @else if (reportData().error()) {
<p>Error loading report.</p>
} @else if (reportData().hasValue()) {
<report-view [data]="reportData().value()" />
}
`,
})
export class Reports {
reportData = input.required<Resource<ReportData>>();
}
```
NOTE: If a blocking resource throws an error, the router cancels the navigation and emits a `NavigationError` event. Resources wrapped in `nonBlocking()` that error will complete navigation and expose the error via the `resource.error()` signal.
### Redirecting from a resource
If a blocking resource needs to redirect the user (for example, if an item is not found), throw a `RedirectCommand` inside the resource loader. The router will cancel the current navigation and redirect to the specified URL:
```ts
import {inject, resource} from '@angular/core';
import {RedirectCommand, Router, Routes} from '@angular/router';
const routes: Routes = [
{
path: 'user/:id',
component: UserProfile,
resources: (ctx) => {
const router = inject(Router);
return {
user: resource({
params: () => ctx.params()['id'],
loader: async ({params: id}) => {
const user = await fetchUser(id);
if (!user) {
throw new RedirectCommand(router.parseUrl('/not-found'));
}
return user;
},
}),
};
},
},
];
```
## Transitional states during pending navigations
When moving between views (or reloading the same view with new parameters), switching abruptly to a loading skeleton can create a jarring UI flash.
The router automatically masks intermediate `loading` and `reloading` states of resolved resources while a navigation is pending.
If you navigate from `/user/1` to `/user/2`, `UserProfile` stays mounted and continues rendering data from `/user/1` (frozen in its exact state) until `/user/2` resolves. Once `/user/2` settles, the router unfreezes the UI, transitioning directly to the new data with no loading flash.
NOTE: Route resources returned to the router are read-only. Manual `.reload()` calls attempted during an active navigation transition or rollback recovery return `false` to avoid interrupting router transition tracking.
### Rollback recovery on cancellation
If a navigation is cancelled (for example, by a guard), the router reverts the state tree to the previous state. This reversion can cause the resource's signal dependencies (such as route parameters) to revert to their previous values.
Because the parameters changed back, the resource might automatically trigger a new load to fetch data for the old parameters. To prevent flashing a loading state for data that was already visible, the router retains the previous resource snapshot in the UI until the resource has settled in the reverted state.
TIP: Forward the `abortSignal` provided by the resource loader to your asynchronous calls (like `fetch`). When the router rolls back parameters or supersedes navigations, the pending request is cleanly aborted: `loader: ({params: id, abortSignal}) => fetchUser(id, {signal: abortSignal})`.
@@ -774,6 +774,30 @@ describe('Router resources integration', () => {
expect(handleCount).toBe(1);
expect((errorRef as Error).message).toBe('Resource failed!');
});
it('should redirect when a blocking resource throws a RedirectCommand', async () => {
const {harness, router} = await setupRouter([
{
path: 'test',
component: TargetCmp,
resources: () => ({
data: resource({
loader: async () => {
throw new RedirectCommand(TestBed.inject(Router).parseUrl('/redirected'));
},
}),
}),
},
{
path: 'redirected',
component: TargetCmp,
},
]);
await harness.navigateByUrl('/test');
expect(router.url).toBe('/redirected');
});
});
describe('rxResource Integration', () => {