~5 min de lecture

Et si withComponentInputBinding était plus structurant qu’on ne le croit ?

withComponentInputBinding() est un petit helper arrivé avec Angular v16 (puis amélioré en v17+), mais qui peut avoir un impact sur certains de tes choix.

🧩 Le problème avant

Avant, pour récupérer les paramètres de route dans un composant, même en full Signals, il fallait passer par ActivatedRoute et toSignal :


@Component({
  standalone: true,
  template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
  private route = inject(ActivatedRoute);

  readonly userId = toSignal(
  this.route.paramMap.pipe(map(params => params.get('userId') ?? 'anonyme'))
  );
}

Même avec Signals, on reste dépendant de RxJS + boilerplate + complexité.


✅ Ce qu’apporte withComponentInputBinding

Pour activer cette fonctionnalité, il faut ajouter withComponentInputBinding() dans la configuration du routeur global :

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

Et dans le fichier de routes :

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

export const routes: Routes = [
  { path: 'profil/:userId', loadComponent: () => import('./profil.page') }
];

Puis, dans le composant :


@Component({
  standalone: true,
  template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
  readonly userId = input<string>();
}

➡️ Le paramètre :userId est automatiquement injecté dans l’input userId.
➡️ Plus besoin d’injecter ActivatedRoute, ni de transformer quoi que ce soit. ➡️ Valable avec les routes params, queryParams, data et resolver. ➡️ Tes composants deviennent donc plus facilement réutilisables, puisqu'ils ne sont plus directement liés à l'ActivatedRoute.


🧠 Comparaison AVANT / APRÈS

Aspect Avant Après
Lecture des params ActivatedRoute + toSignal() input()
Boilerplate élevé minimal
Dépendances RxJS, ActivatedRoute Angular Signals uniquement
Tests plus difficiles plus simples (inputs injectables)
Clarté de lecture moyenne excellente

⚠️ Un comportement à connaître : valeurs par défaut ignorées

Un piège assez subtil existe :
Si la route n’injecte aucune valeur dans un input défini avec input('valeurParDefaut'), la valeur par défaut est ignorée.

Exemple :


@Component({
  standalone: true,
  template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
  readonly userId = input('anonyme');
}

Mais si la route ne définit pas :userId, le composant reçoit... undefined.

Pourquoi ? Parce qu’Angular assigne explicitement undefined à tout input lié, mais non présent dans la route, ce qui écrase la valeur par défaut.

Ce comportement a été discuté dans une issue GitHub où j’ai proposé une amélioration pour conserver les valeurs par défaut quand aucun paramètre n’est fourni.

Cela a été refusé à l'époque, mais Angular 22 a fini par livrer une option qui traite le cas (voir plus bas).

Et input.required() ne te protège pas non plus

Le réflexe naturel face à ce comportement, c'est de rendre l'input obligatoire : si la route ne fournit rien, input.required() devrait crier. Il ne crie pas.


@Component({
  template: `Profil de {{ userId() }}`
})
export default class ProfilPage {
  readonly userId = input.required<string>();
}

Avec la route { path: 'profil', component: ProfilPage } (pas de :userId), le template rend Profil de , userId() vaut undefined, et aucune NG0950 n'est levée - y compris en mode dev, là où on l'attendrait.

La raison est mécanique. input.required() démarre sur une sentinelle : une valeur interne réservée qui ne signifie pas undefined, mais « personne n'a encore écrit ici ». NG0950 n'est levée que tant que cette sentinelle est en place. Or ici le Router écrit : il écrit undefined, ce qui la remplace. Une valeur écrite, fût-elle undefined, sort définitivement l'input de l'état « pas encore fourni » - rien ne restaure jamais la sentinelle. Le garde-fou est désamorcé par le mécanisme même qui cause le bug.


✅ Angular 22 : l'option unmatchedInputBehavior

Mise à jour (Angular 22) : withComponentInputBinding() accepte désormais un objet d'options, dont unmatchedInputBehavior, qui traite le problème à la racine. L'option n'existe pas en Angular 21 et en dessous. Le tour d'horizon de la release est dans le guide Angular 22.

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

Deux valeurs possibles :

  • 'alwaysUndefined' (défaut, comportement historique) : le Router écrit undefined dans tout input non fourni.
  • 'undefinedIfStale' : le Router n'écrit undefined que si la clé a déjà été fournie pendant la vie de la route active dans cet outlet - c'est le cas stale, la donnée devenue obsolète, qu'il faut bien nettoyer. Un input que la route n'a jamais renseigné n'est, lui, jamais écrit du tout.

Ce que ça change, mesuré sur Angular 22 :

Situation alwaysUndefined (défaut) undefinedIfStale
input('anonyme') jamais fourni undefined 'anonyme'
input('anonyme') fourni à la navigation précédente, absent de celle-ci undefined undefined
input.required() jamais fourni undefined, sans erreur NG0950

Lis bien la colonne Situation : les deux garanties valent pour une clé que la route ne fournit jamais - précisément le trou que cet article décrit. Là, la valeur par défaut survit et required retrouve son NG0950.

Dès qu'une clé a été fournie une fois pendant l'activation courante de l'outlet, en revanche, undefinedIfStale se comporte comme alwaysUndefined : elle réécrit undefined. C'est le but - la donnée obsolète doit partir - mais sur un input required, elle l'écrase là aussi en silence, sans NG0950.

Encore faut-il que l'outlet garde la même route activée - passer par une autre route repart de zéro et la valeur par défaut revient. Autrement dit : les query params qui vont et viennent sur une même page - filtres, recherche, tri, pagination. C'est le cas d'usage canonique de withComponentInputBinding(), pas un cas marginal.


🛠 Contournements avant Angular 22

Si tu es encore en Angular 21 ou en dessous, voici les trois options.

Valeur par défaut dans la route

Pour éviter ce comportement, il faut toujours fournir une valeur dans la configuration de la route, par exemple :

export const routes: Routes = [
  {
    path: 'profil',
    // injecté dans l'input
    data: { userId: 'anonyme' },
    loadComponent: () => import('./profil.page'),
  }]

computed / linkedSignal

Ou bien gérer l’input côté composant :

export class ProfilPage {
  readonly userId = input<string | undefined>();

  readonly safeUserId = computed(() => this.userId() ?? 'anonyme');
}

Input transform

Ou encore utiliser la fonction de transformation de ton input :

export class ProfilPage {
  readonly userId = input<string | undefined, string>(undefined, {
    transform: (value) => (value === undefined ? '2' : value),
  });
}

🧱 Conclusion

  • withComponentInputBinding() rend tes composants plus concis.
  • Il s’intègre parfaitement avec les Signals.
  • Et il faut être attentif aux valeurs non fournies, sous peine d’écraser les valeurs par défaut.
  • Ne compte pas sur input.required() pour te protéger : sous le comportement par défaut il ne lève rien et te rend undefined, exactement l'inverse de l'intuition.
  • En Angular 22, pose unmatchedInputBehavior: 'undefinedIfStale' : les valeurs par défaut survivent, et required retrouve son NG0950 - pour les clés que ta route ne fournit jamais, pas pour celles qui disparaissent en cours de route.
  • En Angular 21 et en dessous, pars du principe que la valeur peut être undefined et type tes inputs en conséquence.

Tu veux aller plus loin sur le Routing Angular, en pratique ?
➡️ Découvre le Module 5 d’EasyAngularKit - Maîtriser le routing (v19)

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.