In Angular, how does withComponentInputBinding() bind route state to a routed component's inputs, and why can an input's default value disappear?
answer
- provideRouter feature
- keys match input names
- resolvers beat params beat query
- missing key sets undefined
basics
~20 swithComponentInputBinding() makes the router set the routed component's inputs from query params, path and matrix params, static and resolved data, matched by input name. By default an unmatched input is set to undefined, overwriting its declared default.
solid answer
~40 sAdd it with `provideRouter(routes, withComponentInputBinding())`. Whenever the route's params, query or data change, including on a reused instance, the router merges the route's query params, path and matrix params, static `data` and resolved data, and calls `setInput()` on the component the `router-outlet` activated for each declared input whose public name matches a key. On collisions, resolved and static data win over path params, which win over query params. By default every unmatched input is set to `undefined`, so `page = input(1)` reads `undefined` on a URL without `?page`. Give defaults through an input `transform`, or since v22 pass `unmatchedInputBehavior: 'undefinedIfStale'`. `{queryParams: false}` stops query binding. Only the routed component is bound, not its children.
code
ts · 17 linesimport {Component, computed, input, numberAttribute} from '@angular/core';
import {RouterLink} from '@angular/router';
// Enabled with: provideRouter(routes, withComponentInputBinding())
@Component({
selector: 'app-user-profile',
imports: [RouterLink],
template: `
<h1>User {{ id() }}</h1>
<a [routerLink]="['/users', id() - 1]">Previous</a>
<a [routerLink]="['/users', id() + 1]">Next</a>
`,
})
export class UserProfile {
readonly id = input.required({transform: numberAttribute}); // from users/:id
readonly isFirst = computed(() => this.id() <= 1);
}go deeper
Know that withComponentInputBinding lets a routed component declare an input named after the route parameter instead of injecting ActivatedRoute.
Explain the sources and their precedence, why unmatched inputs become undefined, and the transform or option that restores a default.
Spot routed components whose unrelated inputs are wiped by the router, and choose between input binding and ActivatedRoute per component.
Decide whether a codebase adopts input binding wholesale, weighing its test simplicity against the undefined-default trap and name collisions.
## What the feature does `withComponentInputBinding()` is a feature for `provideRouter()` in `@angular/router` (the `RouterModule.forRoot` equivalent is the `bindToComponentInputs` option). When it is enabled, the component that a `router-outlet` activates gets its **inputs set by the router** from the route's state. Instead of injecting `ActivatedRoute`: ```ts export class UserProfile { readonly id = input.required<string>(); // from users/:id readonly tab = input<string>(); // from ?tab=... } ``` The router calls `setInput()` on the component whenever the relevant route Observables emit, so a **reused** instance receives the new `id` when the user clicks Next, and `computed()` or `effect()` code that reads `id()` follows along. ## Where values come from, and who wins The router builds one object from several sources and binds by key: | Source | Precedence | |---|---| | resolved data (`resolve`) and static `data` | highest | | path parameters and matrix parameters | middle | | query parameters | lowest | So for `/users/42?id=7` on `users/:id`, the `id` input receives `'42'`. The key must match the input's **public name**, meaning its alias if it has one. Values from the URL are strings; data and resolver values keep their types. In 22.2 the router's developer-preview route resources also bind here, above everything else. ## The disappearing default By default, every declared input with **no matching key** is set to `undefined` on each binding pass. That is deliberate: when a query param is removed from the URL, the input must not keep the old value. The side effect surprises people: ```ts readonly page = input(1); // reads undefined on /users, not 1 ``` The fixes: 1. **Transform** - `input(1, {transform: (v: string | undefined) => Number(v ?? 1)})`, so conversion and default live together. 2. **`linkedSignal`** - keep a local signal seeded from the input with a fallback. 3. **`unmatchedInputBehavior: 'undefinedIfStale'`** (since v22) - the router sets `undefined` only for keys that appeared in the route's data earlier in the active route's life in that outlet, so inputs the route never supplies keep their own defaults. ## Options Since v22 the feature accepts an options object: - `queryParams` - `true` by default; `false` stops query params from binding, useful when input names collide with common query keys; - `unmatchedInputBehavior` - `'alwaysUndefined'` (default) or `'undefinedIfStale'`. ```ts provideRouter(routes, withComponentInputBinding({queryParams: false, unmatchedInputBehavior: 'undefinedIfStale'})); ``` ## Limits worth knowing - Only the **routed component** is bound, the one the outlet activated. A `UserCard` inside its template does not receive route inputs; pass values down normally. - Every declared input of a routed component is in the router's hands. An input meant for something else is still overwritten with `undefined` unless you use `'undefinedIfStale'`. - Inputs make testing easy: a test sets `id` with `fixture.componentRef.setInput()` instead of faking an `ActivatedRoute`. ## How it behaves on a reused instance On `/users/1` to `/users/2` the router keeps the `UserProfile` instance, updates its `ActivatedRoute`, and the binding subscription sees the new params and calls `setInput('id', '2')`. For a signal input that means `id()` changes and everything derived from it recomputes; for a decorator `@Input()` it means `ngOnChanges` fires with the new value. The first binding happens synchronously when the component is activated, so the input is set before the first change detection of the view and templates never render a missing id on the first frame. ## When to choose it Input binding suits signal-based components: the id is just an input, and derived state is a `computed()`. `ActivatedRoute` is still the tool when you need values from a parent or child route, the fragment, or the `url` segments, or when the component is not the one the outlet activated.
- Does a component inside the routed component's template receive route inputs?No. The router binds only the component that the router-outlet activated. Children receive values through normal input bindings from that routed component, or inject ActivatedRoute themselves.
- How do you stop query parameters from overwriting inputs with the same name?Pass {queryParams: false} to withComponentInputBinding() to exclude the query string, or rename the input. Path params and data still bind, and path params already take precedence over query params on a key collision.
saying these in an interview costs you the question
- Expecting an input's declared default to survive when the route lacks that key
- Believing query params override path params with the same name
- Thinking every component in the routed view receives route inputs
- Assuming bound URL values arrive already converted to numbers
- Believing input binding stops working when the component instance is reused