skip to content

In Angular 22, when should an invoice page's data come from a ResolveFn, from the component itself, or from the router's route resources?

level: seniorimportance: should knowfreq 50%

answer

  1. who waits: the click or the page
  2. loading and error states live where
  3. redirect before render
  4. withRouterResources, nonBlocking(), developer preview

basics

~20 s

Use a ResolveFn when the page is meaningless without the data or must redirect before rendering; fetch in the component when a skeleton is acceptable; route resources (developer preview in 22.2) add concurrency, optional non-blocking loads and reload without renavigating.

solid answer

~40 s

A `ResolveFn` blocks the navigation, so the component never sees a loading or error state and can be redirected before it exists; the price is a click with no feedback and a parent-to-child waterfall, and data refreshes only by navigating again. Fetching in the component, for example with a signal-based resource bound to the route id, renders immediately with a skeleton and keeps loading and error handling local, but the page flashes an empty state and a not-found case is handled after rendering. Angular 22.2 adds route `resources` behind `withRouterResources()`, still developer preview: they are blocking by default, run concurrently across matched routes, can be marked `nonBlocking()`, and can be reloaded without a navigation. I pick resolvers for small must-have data, component fetching for heavy or secondary data.

code

ts · 30 lines
ts
import { inject, resource } from '@angular/core';
import { nonBlocking, provideRouter, Routes, withComponentInputBinding, withRouterResources } from '@angular/router';
import { InvoiceApi } from './invoice-api';
import { InvoiceEdit } from './invoice-edit';

const routes: Routes = [
  {
    path: 'invoices/:id/edit',
    component: InvoiceEdit,
    resources: (ctx) => {
      const api = inject(InvoiceApi);
      return {
        // blocking: InvoiceEdit gets the value as its `invoice` input
        invoice: resource({
          params: () => ctx.params()['id'],
          loader: ({ params: id }) => api.fetchInvoice(id), // returns a Promise
        }),
        // non-blocking: InvoiceEdit gets the Resource and shows its own loading state
        history: nonBlocking(
          resource({
            params: () => ctx.params()['id'],
            loader: ({ params: id }) => api.fetchHistory(id), // returns a Promise
          }),
        ),
      };
    },
  },
];

export const routerProviders = [provideRouter(routes, withComponentInputBinding(), withRouterResources())];

go deeper

for a junior

Know that a resolver makes the user wait on the old page, while component fetching shows the new page with a loading state.

for a middle

Compare the two by where loading, error and redirect handling live, and mention the waterfall that nested resolvers create.

for a senior

Pick per data item: must-have and small in a resolver or blocking resource, heavy or secondary in the component, with a global navigation indicator.

for a principal

Decide whether the team adopts developer-preview route resources now, weighing concurrency and in-place reload against API churn risk.

## The question behind the question Every routed page has to decide **who waits**: the click (the old page stays until data arrives) or the page (the new page appears at once and fills in). Angular gives three concrete tools, and interviewers want to hear the trade-offs in Angular's terms rather than a general discussion of fetch timing. ## Option 1: a `ResolveFn` The router runs the resolver after guards and before activation. - **Pros:** the component is created with its data, so it has no loading or empty branch for it; a missing invoice becomes a `RedirectCommand` before anything renders; with server-side rendering the HTML is produced with the data present. - **Cons:** the navigation is **blocked**, so without a global indicator (for example one driven by `Router.currentNavigation()`) a slow request looks like a dead click; nested resolvers run **parent before child**, a waterfall; any error fails the whole navigation; refreshing data requires a new navigation that passes the route's `runGuardsAndResolvers` policy. ## Option 2: fetching in the component The route passes only identifiers (for example the `id` as a signal input via `withComponentInputBinding()`), and the component starts its own request, typically with a signal-based resource keyed on that id or with `HttpClient` in the component or a service. - **Pros:** the navigation completes at once and the page shows a skeleton; loading, error and retry are handled right where the data is shown; reloading is just a method call or a changed signal. - **Cons:** every such component needs explicit loading and error states; a not-found invoice can only be handled after the page has rendered; the request starts only after the lazy chunk has loaded and the component has been created. ## Option 3: route resources (Angular 22.2, developer preview) Angular 22.2 exposes a `resources` property on a route, enabled with `withRouterResources()` in `provideRouter`. The function receives a `ResourceContext` with signals for `params`, `queryParams`, `fragment` and `data`, runs in an injection context and returns a record of `Resource` objects. | Behaviour | `resolve` (ResolveFn) | route `resources` | |---|---|---| | Blocks navigation | always | by default; `nonBlocking(res)` opts out | | Order across nested routes | parent, then child | concurrently across matched routes | | What the component receives | the value | the value when blocking; the whole `Resource` when non-blocking | | Refresh without navigating | no | `reload()` on the resource, or a changed signal in `params` | | Redirect | return or throw `RedirectCommand` | throw `RedirectCommand` in the loader | | API status in 22.2 | stable | developer preview | While a navigation is pending, the router **freezes** the resources it exposes on `ActivatedRoute`, so a reused component keeps showing the old invoice until the new one is ready rather than flashing a loading state. Because the API is in developer preview, its shape may still change before it is declared stable; that is a reason to adopt it deliberately, not by default. ## What stays the same across all three Whichever option is chosen, some things do not change. The request itself is still an `HttpClient` call or a `fetch`, usually in a service, so it can be reused and tested outside the routing layer. Authorisation still belongs in a guard, which runs before any resolver or route resource. And server-side rendering still needs the data on the server before the HTML is produced: a blocking resolver or blocking route resource gives that directly, while component fetching relies on the rendering pipeline waiting for pending work. Keeping the fetch logic in a service makes it cheap to move a data item between the three options later. ## A decision rule that holds up in interviews 1. Is the page meaningless without this data, and is the data small and fast? Use a **resolver** (or a blocking route resource) and add a global navigation indicator. 2. Must a missing record redirect before any rendering? That also argues for a resolver. 3. Is the data heavy, slow, or secondary (charts, history, comments)? **Fetch it in the component** and show a skeleton, or mark a route resource `nonBlocking()`. 4. Do nested routes each need independent data? Avoid chained resolvers; use component fetching or concurrent route resources. 5. Does the user refresh the data in place? Resolvers need a renavigation; component resources or route resources can reload directly. A mixed approach is common and defensible: resolve the invoice header that decides whether the page exists, and let the line-item history load inside the page.

  • How do you give users feedback while a resolver blocks navigation?
    Drive a progress bar in the root component from the router: `Router.currentNavigation()` is a signal that is non-null while a navigation is in flight, so `computed(() => !!router.currentNavigation())` can toggle it. Filtering `Router.events` for `NavigationStart` and the end events also works in older code.
  • Why is an invoice-not-found case easier with a resolver than with component fetching?
    The resolver runs before the route activates, so it can return or throw a `RedirectCommand` and the edit page is never created. With component fetching the page has already rendered, so the not-found case must be rendered in place or trigger a navigation after the fact, which briefly shows the wrong page.

saying these in an interview costs you the question

  • Resolvers always make pages feel faster because data is prefetched
  • Component fetching can redirect before the page renders
  • Route resources are stable API in Angular 22.2
  • Nested resolvers run concurrently across parent and child routes
  • A non-blocking route resource still delays activation until loaded