Build1 publisher2 min readPublished Updated
Passing the whole ctx.params() object makes an Angular route resource reload on every navigation
Angular's new resources route property is opt-in behind withRouterResources() and blocks route activation by default. What it reloads on depends on exactly what the params function returns, and the router rebuilds its params object on every navigation.
The Engineer · Build desk
What happened
- Angular ships a Signals-based alternative to route resolvers as a resources route property, opt-in behind withRouterResources() passed to provideRouter at bootstrap.
- The resources property takes a function returning a map of Resource instances, with a ResourceContext handing it params, queryParams, data and a static snapshot, all as signals.
- Wrapping a resource in nonBlocking(), imported from @angular/router, lets the router activate the route without waiting for the loader to settle.
- Any Resource implementation is accepted in a route, so an RxJS-heavy team can use rxResource() by swapping loader for stream.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Teams have to decide how components take route data: enable both router features and get a plain input, or enable the gate alone and read ActivatedRoute.resources by hand in every component.
- cost With the blocking default, loader latency is paid inside the navigation, so a slow endpoint shows up as a link that appears to do nothing until the data lands.
- capability A route's data can now be refreshed in place through the resource's reload(), where the resolver equivalent required a full navigation to run again.
- constraint Error and loading handling for a blocking route cannot live in that route's component, because the component is only created once the value exists.
`params: () => ctx.params()` is the line to flag in review. It type-checks and it does the wrong thing: the router creates a fresh params object on every navigation, so the resource sees a new reference and reloads even when the sku did not change [9]. Writing `params: () => ctx.params()['sku']` fixes it, because the resource then reacts only to the primitive value [10]. Under the object form, any navigation into the route costs one loader call whether the sku moved or not; under the primitive form, a navigation that leaves the sku alone costs none [21]. A bug that renders the right data while doubling the requests behind it tends to survive review.
Blocking also decides where the loading state lives. The router handles loading and error before the component exists, and the input arrives as the resolved value `T` and not `Resource<T>` [14]. A blocking route therefore keeps its spinner and its failure path outside the component, and a route that wants them in its own template wraps the resource in `nonBlocking()` [15]. In my view blocking is the right default for a detail page with nothing to draw before the record arrives; a route whose shell is useful on its own is where the extra template branch is worth writing.
The post names `router_resource.ts` as the file covering everything between `resource({...})` in the route config and the value arriving in the component [17]. It says three things come out of that file: how blocking works, why the UI does not flash a loading skeleton when you navigate between two similar pages, and where a navigation cancellation can leave you stuck if you do not understand the reload guard [18]. Blocking is the one worked through in the published text, which breaks off mid-snippet inside the `nonBlocking()` example [19]. The other two are stated as conclusions.
Adoption is two lines at bootstrap. `withRouterResources()` opens the gate, and `withComponentInputBinding()` is what delivers a resolved resource as a plain `input()`; without the second, components read `ActivatedRoute.resources` by hand [4][5][20]. The `resources` function runs in an injection context, so `inject(ProductService)` sits at the top of it with no factory wrapper [8]. It can be `async` when the loader needs a dynamic import first [11].
The case made against resolvers is sequencing and refresh: they run one after the other, parent to child, and cannot be refreshed without triggering a full navigation [2]. The dev.to write-up calls that "a price you pay on every navigation" [3].
What to watch
- Whether withRouterResources() stops being an opt-in feature when route resources reach a stable Angular release.
- Whether the official data-fetching guide documents the reload guard's behaviour after a cancelled navigation.
- Whether Angular adds a dev-mode warning or lint rule for a params function that returns the whole params object.