Angular's Router Doesn't Have to Wait: Non-Blocking Route Resources

Learn how Angular 22.2 route resources can activate a route before secondary data arrives, with signal-based loading, error, cancellation, and reload states.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

09.10.2026

8 min read

Angular's Router Doesn't Have to Wait: Non-Blocking Route Resources
share

A user clicks Reports. The old page stays on screen, the URL does not change, and nothing appears to happen.

The application has not crashed. Its router is waiting for a slow API request in a ResolveFn before it activates the reports route. That wait turns network latency into navigation latency. It also prevents the reports page from showing a local loading state because Angular has not created the page yet.

Sometimes that is exactly what we want. If a route cannot render without its data, waiting protects the component from an incomplete state. Our reports page is different. It can already show its heading, filters, navigation, and a skeleton while the report loads. Delaying all of that makes the route feel stuck even though only one part of it is waiting for data.

This is the problem non-blocking route resources solve. Introduced in Angular 22.2, they let the router activate a route immediately and bind a signal-based resource to the component. The request still takes the same amount of time, but the page can now explain that wait with its own loading, error, and value states.

nonBlocking() and router resources are developer-preview APIs as of Angular 22.2. Check the current API before adopting them in a long-lived library.

The official route resources guide and nonBlocking() API reference are the source of truth for the examples below. We will begin with the resolver that causes the pause, then change one decision at a time until the page can manage the request itself.

Why a resolver can make navigation feel stuck

A traditional resolver runs before route activation:

import { inject } from '@angular/core';
import { ResolveFn, Routes } from '@angular/router';

const reportResolver: ResolveFn<Report> = () => {
  const reportsApi = inject(ReportsApi);
  return reportsApi.getReport();
};

export const routes: Routes = [
  {
    path: 'reports',
    component: ReportsPage,
    resolve: { report: reportResolver },
  },
];

This contract is pleasantly simple. The component receives a complete Report, so it never has to account for a missing value. The cost sits outside the component: navigation cannot finish until getReport() finishes. A slow network therefore delays the entire route transition, not only the report.

You can show a global navigation indicator by listening to router events, but the reports page still cannot render its own skeleton because it does not exist yet. Angular's resolver guide calls out this trade-off directly.

That tradeoff does not make resolvers obsolete. It gives us a useful boundary: data that defines whether a route can exist belongs before activation, while data that fills in an already useful page does not necessarily belong there. Route resources let us keep that distinction while moving data loading into Angular's signal model.

Route resources bring data loading into the signal model

The resources route property accepts a function that returns named Angular resources. Like a resolver, the function runs in an injection context and belongs to the route configuration. Unlike a resolver, each entry is an Angular resource with signal-based state.

The function also receives a ResourceContext. Its route parameters, query parameters, fragment, and route data are signals, so a resource can react to the part of the URL that drives its request.

Before using it, enable router resources and component input binding:

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()),
  ],
};

withRouterResources() enables the resources configuration. withComponentInputBinding() then binds each named entry to a matching component input. No NgModule is needed.

With those providers enabled, we can replace the resolver route's resolve field with resources:

import { resource } from '@angular/core';
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'reports',
    component: ReportsPage,
    resources: () => ({
      report: resource({
        loader: ({ abortSignal }) => loadReport(abortSignal),
      }),
    }),
  },
];

async function loadReport(abortSignal: AbortSignal): Promise<Report> {
  const response = await fetch('/api/report?delay=3000', {
    signal: abortSignal,
  });

  if (!response.ok) {
    throw new Error(`Report request failed: ${response.status}`);
  }

  return response.json() as Promise<Report>;
}

The difference from the opening example is easy to miss but important. resolve maps the report key to a ResolveFn that produces the final value. resources runs a factory that creates a Resource for that key. The router can now work with the resource's signals, status, cancellation, and reload behavior instead of seeing only the eventual Report.

Route resources can use resource(), httpResource(), rxResource(), or a custom Resource implementation. Because their parameters are signals, a loader can react to a specific route parameter or query parameter without a manual subscription. Angular documents the complete context in the ResourceContext API.

resource() or httpResource()?

The example above uses plain resource() because it makes the loader and its abortSignal visible. It is also the more general option. Use it when the asynchronous work is not an HTTP request, when an SDK returns a promise, or when the loader needs to coordinate several operations before producing one value.

For a straightforward HTTP read, httpResource() removes some plumbing. It uses Angular's HttpClient stack, including interceptors and the usual HTTP testing tools, and it cancels an outstanding request when its reactive request changes. The same blocking rules apply, so the report route could use it directly:

import { httpResource } from '@angular/common/http';

resources: () => ({
  report: httpResource<Report>(() => '/api/report?delay=3000'),
}),

Choose httpResource() when the request maps cleanly to a URL or HttpResourceRequest and should go through HttpClient. Choose resource() when the loader needs custom asynchronous logic or a source outside HTTP. If an existing service already returns an Observable, rxResource() avoids converting it to a promise. These resource APIs are intended for reads. For mutations such as POST or PUT, call HttpClient directly instead of modeling the command as a resource. Angular's httpResource guide covers request options, response parsing, and testing.

Choosing the resource implementation is separate from choosing whether the route should wait. Both examples above still block navigation because blocking is the default. Either resource can be wrapped in nonBlocking(), which is the choice we will make next for the slow report.

Three ways to load the same report

We have now seen the first two approaches. The resolver from the opening section and the route resource above both wait before activation, but they give Angular different ways to manage the request. Adding nonBlocking() gives us a third option.

The important difference is what the router waits for, what the component receives, and where an error appears.

Approach Route activation Component receives Loading and error UI
Traditional ResolveFn Waits for the resolver Resolved Report through route data or input binding Usually handled at navigation level
Blocking route resource Waits for the resource Resolved Report value A resource error can cancel navigation
Non-blocking route resource Activates immediately Resource<Report> Component reads loading, error, and value signals

In the blocking route-resource example above, component input binding unwraps the resource. The page declares report = input.required<Report>(), not a resource input, and Angular activates the component only after the value exists. We keep the component's simple data contract while gaining signal-aware route configuration.

The failure behavior remains tied to navigation. If the loader fails, Angular cancels the navigation with a NavigationError. That is useful for navigation-critical data, but it is a poor fit for a secondary chart that could show an inline retry button.

For our reports page, we want the opposite contract. The route should activate first, and the report section should own the request state. Wrapping the resource in nonBlocking() makes that change explicit:

import { resource } from '@angular/core';
import { nonBlocking, Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'reports',
    component: ReportsPage,
    resources: () => ({
      report: nonBlocking(
        resource({
          loader: ({ abortSignal }) => loadReport(abortSignal),
        }),
      ),
    }),
  },
];

The artificial delay makes the UI states easy to test. It is not a performance claim. nonBlocking() does not reduce the API's network latency. It removes that request from the route's critical navigation path.

That one wrapper also changes the component contract. Instead of receiving a finished Report, the page receives the resource and decides what each state should look like.

Render the resource state in the page

For a non-blocking resource, the router binds the resource object instead of unwrapping its value. The page gains a little more responsibility, but it also gains the information it needs to respond without delaying navigation.

import { Component, input, Resource, WritableResource } from '@angular/core';

interface Report {
  generatedAt: string;
  revenue: number;
  activeCustomers: number;
}

type ReloadableResource<T> = Resource<T> & Pick<WritableResource<T>, 'reload'>;

@Component({
  selector: 'app-reports-page',
  templateUrl: './reports-page.html',
})
export class ReportsPage {
  readonly report = input.required<ReloadableResource<Report | undefined>>();
}

Resource<T> exposes signal-based isLoading, error, hasValue, value, and status state. The small ReloadableResource type keeps that state read-only in the component while exposing the one command the UI needs: reload().

Those signals map directly to what the user should see. The template can express that mapping with Angular's built-in control flow:

<header>
  <div>
    <p>Analytics</p>
    <h1>Reports</h1>
  </div>

  <button
    type="button"
    (click)="report().reload()"
    [disabled]="report().isLoading()"
  >
    Refresh report
  </button>
</header>

@if (report().isLoading() && !report().hasValue()) {
<section class="report-skeleton" aria-label="Loading report">
  <div class="skeleton skeleton-heading"></div>
  <div class="skeleton skeleton-chart"></div>
</section>
} @else if (report().error(); as error) {
<section role="alert">
  <h2>We could not load this report</h2>
  <p>{{ error.message }}</p>
  <button type="button" (click)="report().reload()">Try again</button>
</section>
} @else if (report().hasValue()) {
<section>
  <p>Generated {{ report().value().generatedAt }}</p>
  <dl>
    <dt>Revenue</dt>
    <dd>{{ report().value().revenue }}</dd>
    <dt>Active customers</dt>
    <dd>{{ report().value().activeCustomers }}</dd>
  </dl>
</section>
}

During the first request, there is no value to display, so the page shows a skeleton. If the request fails, the failure stays inside the report section and the user gets a retry action. Once the resource has a value, the same section renders the report.

The refresh button demonstrates another benefit of keeping the resource. Calling reload() runs the loader again without navigating away from the page. During this imperative reload, the resource enters the reloading state and keeps its previous value. The existing report can remain visible while the disabled button shows that a refresh is in progress. The resource guide documents this behavior and the full set of status values.

Forward the abort signal

Reloading handles a request the user still wants. Cancellation handles the opposite case: a request whose result is no longer useful. Every resource loader receives an abortSignal, and we should pass it to fetch, as the example does.

If the user starts another navigation, route parameters change, or the router rolls back a cancelled navigation, Angular can then stop work that no longer has a consumer. Without the forwarded signal, the browser may continue the request even though Angular will ignore its result.

Cancellation avoids unnecessary work. Like nonBlocking(), it does not turn a slow endpoint into a fast one.

The same rule becomes more important when the report depends on a route parameter. Moving from one report to another should cancel the first request before starting the next one:

resources: (context) => ({
  report: nonBlocking(
    resource({
      params: () => context.params()['reportId'],
      loader: ({ params: reportId, abortSignal }) =>
        fetch(`/api/reports/${reportId}`, { signal: abortSignal }).then(
          async (response) => {
            if (!response.ok) {
              throw new Error(`Report request failed: ${response.status}`);
            }

            return (await response.json()) as Report;
          },
        ),
    }),
  ),
});

The params function reads only reportId, so the loader reacts to the value that controls this request. Returning the entire params object would create a new dependency value on each navigation and could reload the report when an unrelated parameter changes.

Route resources remove parent-to-child waterfalls

So far, our example has one resource on one route. The difference becomes more noticeable in a nested route tree.

Resolvers run from parent routes to child routes. This ordering is necessary when a child resolver needs its parent's resolved data, but it also turns independent requests into a waterfall.

Route resources across a matched route hierarchy run concurrently. A parent layout resource and a child report resource can start together instead of waiting for each other. Navigation waits for any blocking resources in that group, while non-blocking resources continue after activation.

This concurrency does not make either request faster. It removes avoidable sequencing between independent requests. If one resource truly depends on another, model that dependency explicitly instead of relying on timing.

When blocking is still the right choice

Blocking is a much narrower choice than "the page needs this data." Almost every data-heavy page needs its data eventually, but it can usually render a heading, controls, and a skeleton first.

A document editor is a good example of why this distinction matters. Even if it cannot create the editing controls before the document arrives, it can still activate the route, render the workspace layout, and show a loading state in the editor area. If loading fails, the page has a natural place for an error and retry action. Blocking would keep the user on the previous route without improving the request itself.

Blocking earns its place when the result can change whether or where navigation should complete. A resource that loads an entity and redirects to /not-found when it does not exist is one example. A blocking resource can throw a RedirectCommand before the target component activates, so the user never sees a page for an invalid entity. The same reasoning applies when the server must validate a workflow state or tenant context and an invalid result sends the user somewhere else.

Authentication and authorization deserve a separate note. Use CanActivate or CanMatch guards when the router must decide whether a user may access or match a route. A route resource can load data after that decision, but it should not replace the guard. Client-side guards and resources are user-experience controls, not security boundaries, so the server must enforce the same access rules. Angular's route guard guide makes that warning explicit.

There is also a deliberate UX case for blocking: keeping the current page visible until the next page is complete, with a global navigation indicator during the transition. That can avoid a loading flash when moving between closely related routes. It should be a conscious product decision, not the default response to a component that needs data.

The error behavior is a useful test for the decision. A blocking resource error cancels navigation. A non-blocking resource completes navigation and exposes the failure through error(), which means the component must have somewhere meaningful to render it.

Resolvers remain a supported, direct tool for pre-activation data. Blocking route resources add signal integration and concurrent hierarchy loading. Non-blocking route resources move async states into the component. These are separate choices, not a migration ladder where the newest API automatically wins.

The practical rule

The reports example leaves us with a sharper question for every route request: can this result change whether or where navigation should complete?

If the answer is yes, blocking may be the right choice. Use a guard for route access and matching. Use a resolver or blocking route resource when loading the data itself can cancel or redirect the navigation.

If the answer is no, activate the page and load the display data with a non-blocking route resource. Give the waiting part of the page an honest skeleton, a useful error state, and a retry button. The request may still be slow, but the route no longer has to pretend that nothing is happening.

Block only when the result is part of the navigation decision. Load page content with a non-blocking route resource.

Route resources make the most sense once signals feel natural as a way to model asynchronous state. Kevin's Angular Signals eBook develops that mental model further, including resources, inputs, and signal-driven Angular code.

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Build smarter UIs with Angular + AI

Angular + AI Video Course

Angular + AI Video Course

A hands-on course showing how to integrate AI into Angular apps using Hash Brown to build intelligent, reactive UIs.

Learn streaming chat, tool calling, generative UI, structured outputs, and more — step by step.

Prepare yourself for the future of Angular and become an Angular Signals expert today!

Angular Signals Mastercalss eBook

Angular Signals Mastercalss eBook

Discover why Angular Signals are essential, explore their versatile API, and unlock the secrets of their inner workings.

Elevate your development skills and prepare yourself for the future of Angular. Get ahead today!

Do you enjoy the content and want to master Angular's brand new Signal Forms?

Angular Signal Forms: Hands-On Masterclass

Angular Signal Forms: Hands-On Masterclass

Master Angular's brand new Signal-Forms through 12 progressive chapters with theory and hands-on labs.

Learn form basics, validators, custom controls, subforms, migration strategies, and more!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Modern Angular or Signals !

Stärken Sie Ihr Team mit unserer umfassenden Erfahrung

Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

or