Skip to main content

5 min read #angular #routing #signals #inputs #frontend

My route params bind straight to inputs now

withComponentInputBinding() wires path params, query params and resolver data to your component inputs by name. Pair it with signal inputs and the ActivatedRoute subscription disappears entirely.

Here's a pattern every Angular dev has typed a hundred times: a component needs the :id from the URL, so you inject ActivatedRoute, subscribe to paramMap, pull the value, remember to unsubscribe, and handle the fact that the component is reused when only the id changes so ngOnInit won't fire twice. Six lines of plumbing to read one string off the URL.

withComponentInputBinding() deletes all of it. You turn it on once:

bootstrapApplication(App, {
  providers: [provideRouter(routes, withComponentInputBinding())],
});

and now the router sets your component's inputs from the route, matched by name. A route param :id binds to an input called id. That's the entire mental model.

@Component({ /* ... */ })
export class OrderDetail {
  id = input.required<string>();   // <- comes straight from :id
}

Four sources feed the same inputs

It's not just path params. The router binds from four places, and once you know all four you stop reaching for ActivatedRoute almost entirely: resolved data and static data on the route, path params (:id), matrix params, and query params (?tab=invoices). A search page reads its filters as inputs; a wizard reads its step; a detail page reads its id. All by name, all without a subscription.

The one thing you have to internalize is precedence, because all four write to the same input slots. If the same key exists in more than one source, resolved/static data wins, then path params, then query params. The practical consequence: don't name a query param the same thing as something in your resolver data, or the data value silently shadows it and you'll swear the query param "isn't binding." Give colliding things distinct names and the whole feature stays predictable.

The real payoff is signal inputs

Input binding is nice on its own, but combined with input() signals it changes how route-driven components are written. The route param isn't a value you grabbed in ngOnInit — it's a signal that updates when the URL changes. Which means you can derive from it and the reactive graph handles the rest:

id = input.required<string>();

// re-runs automatically when the URL id changes — no subscription,
// no ngOnChanges, no manual refetch
order = httpResource(() => `/api/orders/${this.id()}`);

Navigate from order 7 to order 8 and the component instance is reused, the id signal flips to "8", and httpResource refetches because its url depended on id(). The "component reuse won't re-run my init" problem — the one that sent everyone subscribing to paramMap in the first place — just isn't a problem anymore, because nothing was keyed on a lifecycle hook to begin with.

Where it stops, and that's fine

Two honest limits. First, it binds to the routed component — the one named in the route config. A deeply nested child that wants the parent route's params still reads ActivatedRoute, or you pass them down as inputs yourself. Second, this is a router feature, not magic on every component; a component you drop into a template the normal way gets its inputs from the template, as always.

I treat ActivatedRoute the way I now treat ngOnDestroy: still there, occasionally necessary, but no longer the default reflex. For the common case — a routed component that wants a value off the URL — the value just arrives as an input, and if it's a signal input, it stays correct on its own.