<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ali</title>
    <description>The latest articles on DEV Community by Ali (@aelmufti).</description>
    <link>https://dev.to/aelmufti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3936579%2Fc2c24978-0cee-4131-8ee7-2b8f9ea628ef.jpeg</url>
      <title>DEV Community: Ali</title>
      <link>https://dev.to/aelmufti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aelmufti"/>
    <language>en</language>
    <item>
      <title>The gap</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 05 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/the-gap-kpl</link>
      <guid>https://dev.to/aelmufti/the-gap-kpl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvitkaj9lnd11p4unxvm5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvitkaj9lnd11p4unxvm5.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a shape I keep sketching on whiteboards when this comes up, and it's three curves. AI adoption, going up and to the right. Hands-on coding ability in people who started after 2023, going down. And a third curve almost nobody puts on the chart: what the AI actually costs to run, versus what you pay for it. Those two lines are not the same line, and the space between them is borrowed money.&lt;/p&gt;

&lt;p&gt;Play the three curves forward and you get a period — I don't know if it's five years or fifteen — where the people who could build software from their own heads are retiring, the people who should have replaced them never learned, and the tool that was supposed to make the difference irrelevant has been repriced out of casual reach. I call that period the gap. This post is me trying to argue myself out of it, and mostly failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first curve: prompting is not authoring
&lt;/h2&gt;

&lt;p&gt;Let me be precise about what juniors are and aren't learning, because "kids these days can't code" is a complaint as old as COBOL and usually wrong. Today's juniors are not lazy and not stupid — many of them ship more in their first year than I did in my first three. What they're not doing is &lt;em&gt;authoring&lt;/em&gt;: staring at a blank function, holding the problem in their head with no autocomplete to lean on, being wrong alone, and debugging their way out. They review, they steer, they prompt again. The output is real. The formation isn't happening.&lt;/p&gt;

&lt;p&gt;I've watched it up close. A junior on a recent project produced a clean, working feature in an afternoon — and couldn't answer why one of the two API calls in it needed a retry and the other one must never have one. Not because they hadn't read the code. Because that distinction lives in a layer of understanding you only build by having been the author when it went wrong. Skill isn't downloaded from output. It's deposited by struggle, in small amounts, over years. Remove the struggle and the deposits stop, even while the output keeps flowing. That's the trap: the metrics all look fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second curve: you are not paying the real price
&lt;/h2&gt;

&lt;p&gt;Here's the part of my argument people push back on hardest, so let me make it carefully. The twenty dollars a month you pay for an AI assistant is not a price. It's a bet — someone else's bet, placed with venture and cloud-provider capital, that subsidizing your dependency today buys them your budget tomorrow. The major labs lose money serving you. The GPUs are scarce, the energy is real, the datacenters are being financed like railroads. We have seen this movie: rides across Paris for six euros until the day Uber needed to become a business, then the same ride for fourteen. WeWork desks, meal delivery, scooters — every VC-funded convenience of the 2010s followed the same arc. Cheap while they're buying the habit, repriced once the habit is bought.&lt;/p&gt;

&lt;p&gt;The honest counterargument is that inference cost per token has been falling fast, and that's true — models get distilled, hardware improves, yesterday's frontier becomes today's commodity. But three things eat that gain. Usage explodes faster than unit cost falls: agents that run for hours, codebases fed whole into context windows, one prompt fanning out into a thousand calls. The capability you actually depend on for real work stays at the frontier, and the frontier stays premium — nobody bets their production system on the free tier. And once an industry has burned hundreds of billions, the money wants to come back; it comes back through the price, the usage caps, the enterprise tier, the "your plan has changed" email. Cheap tokens for toy tasks, expensive tokens for the work that matters. The subsidy doesn't end with a bang. It ends with a pricing page that gets one row longer every quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the curves cross
&lt;/h2&gt;

&lt;p&gt;Neither curve is a catastrophe alone. A generation that codes less but has cheap abundant AI forever? Fine — that's just a new abstraction layer, and we've absorbed those before; nobody mourns hand-rolled assembly. Expensive AI in a world full of people who can code without it? Also fine — annoying, but the skill is there to fall back on. The gap is what happens when both arrive at once: the crutch gets expensive at the exact moment an entire cohort needs it to walk.&lt;/p&gt;

&lt;p&gt;Picture a mid-size company in that world. Its senior engineers — the ones formed before the shift — are in their fifties, expensive, and leaving. Its ten-year veterans came up prompting; they're excellent orchestrators of a tool the CFO now rations, because the AI line item has quietly become the third biggest cost after salaries and cloud. There's a production incident in a system that was 80% generated three years ago, the agent budget for the month is spent, and the person on call has never once debugged something this deep by hand. That's not science fiction. That's just the three curves, extended, meeting in one on-call rotation.&lt;/p&gt;

&lt;p&gt;The uncomfortable historical rhyme: we've lost operational knowledge this way before. Ask anyone who maintains mainframe COBOL what it costs to hire for a skill the industry stopped teaching forty years ago — the code outlived the people, and the people who remained could name their price. The gap is that dynamic, but not for one legacy niche. For the general ability to write software unassisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gap is not
&lt;/h2&gt;

&lt;p&gt;I want to be fair to the future, because doom-scented essays are cheap. The gap, if it comes, is a &lt;em&gt;transition&lt;/em&gt;, not an end state. Markets route around scarcity: if frontier AI gets rationed by price, open-weight models running on owned hardware become the diesel generator of software teams — worse, but yours. Salaries for humans who can author unassisted would spike, and spiking salaries are the best recruitment poster ever printed; the skill would get relearned, the way it always does when it pays again. And it's possible the cost curve genuinely wins — that inference becomes too cheap to meter and this whole essay ages like predictions of peak oil. I'd take that outcome happily.&lt;/p&gt;

&lt;p&gt;But notice that every one of those escape routes takes years to travel, and none of them helps the person who is on call the night the curves cross. The gap isn't "software ends." It's a decade where the muscle is gone, the prosthetic is expensive, and everything built during the cheap era still needs an adult. Transitions are where the damage happens. Nobody drowned in the sea level; they drowned in the flood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Positioning for it
&lt;/h2&gt;

&lt;p&gt;If you buy even half of this, the hedges are cheap and mostly unfashionable. If you're junior: use the tools — refusing them is career malpractice — but ring-fence real authoring time the way athletes lift weights they'll never lift in a match. Build something with the AI turned off, regularly, and let it hurt. You're not doing it for the output; you're doing it because in a market where nobody can do it by hand, being the person who can is the trade of the decade. Scarcity is a career strategy, and hand-authoring is on sale right now precisely because everyone thinks it's obsolete.&lt;/p&gt;

&lt;p&gt;If you run a team: treat AI spend like debt, not income — it compounds, and the rate can change. Keep an exit ramp: know today which open-weight model plus which hardware covers 70% of your usage if the invoice doubles, because negotiating that migration during a price shock is how you end up paying anything. And keep giving humans work the machine could do — on purpose, as a training budget, not a productivity failure. The companies that cross the gap intact will be the ones that kept the skill alive in humans while it was economically irrational to do so.&lt;/p&gt;

&lt;p&gt;I might be wrong about the prices. I hope I am. But between "the subsidy lasts forever" and "keep the muscle," only one of those is a bet I have to win.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>economics</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Freelance ou CDI : ce que coder le simulateur m'a appris</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Fri, 03 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/freelance-ou-cdi-ce-que-coder-le-simulateur-ma-appris-1lk2</link>
      <guid>https://dev.to/aelmufti/freelance-ou-cdi-ce-que-coder-le-simulateur-ma-appris-1lk2</guid>
      <description>&lt;p&gt;Il y a une phrase que tout développeur salarié a entendue au moins une fois, souvent d'un ami qui vient de se lancer : &lt;em&gt;« Tu passerais freelance, tu gagnerais le double. »&lt;/em&gt; Le calcul derrière est toujours le même — on prend un TJM, on le multiplie par le nombre de jours ouvrés dans l'année, et on compare le résultat à un salaire net. J'ai reçu la question assez de fois pour finir par coder &lt;a href="https://freelance-ou-cdi.fr" rel="noopener noreferrer"&gt;freelance-ou-cdi.fr&lt;/a&gt;, un simulateur open-source qui fait le calcul pour de vrai. Voici ce que construire cet outil — et faire tourner mes propres chiffres dedans — m'a appris.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le calcul naïf est faux dès la première ligne
&lt;/h2&gt;

&lt;p&gt;« TJM × jours ouvrés » se trompe deux fois. D'abord parce qu'un TJM n'est pas un salaire : c'est un chiffre d'affaires. Entre les deux, il y a les cotisations sociales, et selon le statut — micro-entreprise, EI, EURL, SASU, portage — elles ne se calculent ni au même taux ni sur la même assiette. Ensuite parce que l'impôt sur le revenu s'applique &lt;em&gt;par-dessus&lt;/em&gt;, et là encore le statut change tout : une SASU qui arbitre entre salaire et dividendes ne sera pas imposée comme un micro-entrepreneur au versement libératoire. Comparer un CA freelance à un salaire net, c'est comparer un prix hors taxes à un prix payé en caisse.&lt;/p&gt;

&lt;p&gt;C'est la première chose que le simulateur impose : il ne s'arrête pas au brut. Il descend jusqu'au &lt;strong&gt;net réellement disponible, après cotisations et après impôt&lt;/strong&gt; , pour chaque statut, et il affiche le TJM d'équilibre — le tarif journalier qu'il faut atteindre pour, une fois tout payé, égaler un salaire net donné. Pour être sûr que ces chiffres ne sortent pas de nulle part, les calculs sont validés par des tests automatisés contre &lt;code&gt;modele-social&lt;/code&gt;, le moteur open-source publié par l'URSSAF elle-même. Quand mon résultat diverge du leur, c'est mon code qui a tort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vous ne facturez pas 218 jours
&lt;/h2&gt;

&lt;p&gt;L'erreur la plus coûteuse n'est pas dans les charges, elle est dans le dénominateur. Une année compte environ 218 jours ouvrés, et l'intuition pousse à tous les facturer. Sauf que le freelance ne vend pas des jours ouvrés, il vend des jours &lt;em&gt;facturés&lt;/em&gt;. Enlevez cinq semaines qui ressemblent à des congés payés — sauf qu'ici personne ne vous les paie. Enlevez l'intercontrat, ces creux entre deux missions où le compteur tourne à zéro. Enlevez la prospection, la comptabilité, les devis, la veille, la formation que plus aucun employeur ne finance à votre place.&lt;/p&gt;

&lt;p&gt;En pratique, un freelance qui tourne bien facture autour de 200 jours, et beaucoup tournent plus bas la première année. Ça n'a l'air de rien, mais c'est une bascule : à 200 jours au lieu de 218, votre TJM doit déjà monter de près de 10 % rien que pour compenser le calendrier, avant même d'avoir parlé de cotisations. Le simulateur laisse régler ce nombre précisément, parce que c'est le paramètre qui déplace le plus la ligne d'équilibre — et c'est aussi celui que les comparaisons de comptoir oublient systématiquement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le CDI donne sans le dire
&lt;/h2&gt;

&lt;p&gt;Un salaire net, ce n'est pas qu'un montant sur une fiche de paie. C'est un revenu &lt;strong&gt;lissé&lt;/strong&gt; — vous touchez la même somme le mois où vous êtes malade, celui où le projet est en pause, celui où vous partez trois semaines. C'est une assurance chômage si le poste s'arrête. C'est des congés payés, une part de mutuelle, une cotisation retraite prise à la source, un budget formation, parfois une participation ou un treizième mois. Et c'est zéro administratif : pas d'URSSAF à déclarer, pas de TVA à suivre, pas de relance d'un client qui paie à 60 jours.&lt;/p&gt;

&lt;p&gt;Rien de tout ça n'est gratuit côté freelance : ça se rachète, en prévoyance, en mutuelle privée, en épargne de précaution pour tenir les creux, et en temps passé à gérer une micro-entreprise qui est aussi une vraie entreprise. Le simulateur ne chiffre pas votre tranquillité d'esprit, mais il rend visible la partie monétisable de cet écart — et elle est plus grande qu'on ne l'imagine tant qu'on ne l'a pas alignée en colonnes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le résultat qui m'a surpris
&lt;/h2&gt;

&lt;p&gt;Une fois le calendrier réaliste et toutes les charges en place, le constat revient presque à chaque simulation : à séniorité équivalente, l'écart de net entre un bon CDI et un freelance correctement facturé est &lt;em&gt;beaucoup&lt;/em&gt; plus faible que le « tu gagnerais le double » du départ. Pour &lt;em&gt;égaler&lt;/em&gt; — pas dépasser, égaler — un CDI confortable de développeur à Paris, il faut un TJM nettement au-dessus de la simple division du salaire par les jours ouvrés, souvent de l'ordre d'un tiers de plus une fois les jours non facturés et les charges digérés. Le surplus tant vanté du freelance n'apparaît qu'&lt;em&gt;au-dessus&lt;/em&gt; de ce seuil, et il paie d'abord le risque avant d'être du pouvoir d'achat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donc, freelance ou CDI ?
&lt;/h2&gt;

&lt;p&gt;Le simulateur donne un chiffre, mais il ne donne pas la réponse, parce que la réponse n'est pas financière. Une fois l'écart d'argent ramené à sa vraie taille — modeste, à séniorité égale — la décision se joue ailleurs : votre tolérance au risque et aux mois sans revenu, votre appétence pour l'autonomie, et surtout votre capacité à &lt;strong&gt;trouver des missions&lt;/strong&gt; , qui est le vrai métier caché du freelance. Quelqu'un qui déteste vendre et dort mal quand le compte baisse sera plus riche &lt;em&gt;et&lt;/em&gt; plus heureux en CDI, même si la ligne « freelance » du tableau est un peu plus haute. L'inverse est vrai aussi.&lt;/p&gt;

&lt;p&gt;C'est pour ça que l'outil est public et open-source : l'intérêt n'est pas que je vous donne &lt;em&gt;ma&lt;/em&gt; réponse, c'est que vous fassiez tourner &lt;em&gt;vos&lt;/em&gt; chiffres, avec votre salaire, votre TJM cible et votre nombre de jours honnête. Faites le calcul sur &lt;a href="https://freelance-ou-cdi.fr" rel="noopener noreferrer"&gt;freelance-ou-cdi.fr&lt;/a&gt; ; et si vous voulez juste convertir vite un TJM en salaire équivalent dans un sens ou dans l'autre, le &lt;a href="https://dev.to/outils/calculateur-tjm/"&gt;calculateur TJM ↔ salaire&lt;/a&gt; fait ça sans inscription. La bonne décision, c'est celle que vous prenez avec les vrais nombres sous les yeux — pas avec ceux du comptoir.&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>cdi</category>
      <category>tjm</category>
      <category>carrière</category>
    </item>
    <item>
      <title>Crèche ou nounou : le vrai coût commence après les aides</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/creche-ou-nounou-le-vrai-cout-commence-apres-les-aides-3opc</link>
      <guid>https://dev.to/aelmufti/creche-ou-nounou-le-vrai-cout-commence-apres-les-aides-3opc</guid>
      <description>&lt;p&gt;Quand est venue la question du mode de garde, j'ai fait ce que je fais devant n'importe quelle décision à plusieurs milliers d'euros : j'ai ouvert un tableur. Le même réflexe que pour comparer un TJM freelance à un salaire — parce qu'en France, sur ce genre de sujet, le prix affiché ne veut presque rien dire. Entre le tarif que vous lisez et l'argent qui sort réellement de votre compte à la fin de l'année, il y a trois couches d'aides qui rebattent complètement les cartes. Voici le calcul honnête, et ce qu'il ne dit pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trois options, trois logiques de coût
&lt;/h2&gt;

&lt;p&gt;Les trois modes de garde classiques ne se comparent pas sur la même grille :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La crèche&lt;/strong&gt; (collective, municipale ou associative) facture à un tarif &lt;em&gt;indexé sur vos revenus&lt;/em&gt;, via le barème national de la CNAF (participation familiale). Deux familles n'y paient pas le même prix pour la même place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'assistante maternelle agréée&lt;/strong&gt; (la « nounou » qui garde chez elle) est votre salariée : vous la payez au tarif horaire convenu, déclaré via Pajemploi, et vous ouvrez droit à une aide de la CAF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La garde à domicile&lt;/strong&gt; (chez vous) suit la même logique d'emploi direct, mais coûte plus cher — sauf en _garde partagée_entre deux familles, qui divise la facture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparer leurs tarifs bruts, c'est comme comparer des TJM sans regarder les statuts : on additionne des choses qui ne se déduisent pas de la même manière.&lt;/p&gt;

&lt;h2&gt;
  
  
  La première couche : le CMG
&lt;/h2&gt;

&lt;p&gt;Pour l'assistante maternelle et la garde à domicile, la CAF verse le &lt;strong&gt;Complément de libre choix du mode de garde&lt;/strong&gt; (CMG), qui prend en charge une partie de la rémunération. Son montant dépend de vos revenus, de l'âge de l'enfant et du mode de garde. Point important : le CMG a été &lt;em&gt;réformé&lt;/em&gt; pour être linéarisé — au lieu de trois tranches qui créaient des effets de seuil brutaux, l'aide décroît plus progressivement avec le revenu, ce qui a rapproché le reste à charge de l'assistante maternelle de celui de la crèche pour beaucoup de familles. La crèche, elle, ne touche pas de CMG : son « aide » est déjà intégrée dans le tarif indexé que vous payez.&lt;/p&gt;

&lt;h2&gt;
  
  
  La deuxième couche : le crédit d'impôt
&lt;/h2&gt;

&lt;p&gt;Par-dessus le CMG s'ajoute le &lt;strong&gt;crédit d'impôt pour frais de garde&lt;/strong&gt; : 50 % des dépenses restant à votre charge, dans la limite d'un plafond annuel par enfant. « Crédit » et non « réduction », la nuance compte — il vous est remboursé même si vous n'êtes pas imposable. Ce crédit s'applique au reste à charge &lt;em&gt;après&lt;/em&gt; CMG pour l'emploi d'une assistante maternelle, et aux frais de crèche pour la crèche. C'est la couche que les comparaisons oublient le plus souvent, et c'est celle qui divise par deux le chiffre final.&lt;/p&gt;

&lt;p&gt;L'ordre est donc toujours le même : prix affiché → moins le CMG (le cas échéant) → moins 50 % de crédit d'impôt sur ce qui reste. Le nombre auquel vous devez comparer les options, c'est le tout en bas de cette cascade, pas le tarif du haut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le tableau a fini par montrer
&lt;/h2&gt;

&lt;p&gt;Trois constats reviennent, une fois la cascade appliquée avec de vrais revenus :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La crèche est souvent la moins chère pour les revenus modestes&lt;/strong&gt; , précisément parce que son tarif est indexé : plus le revenu est bas, plus la participation est faible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'assistante maternelle se rapproche de la crèche&lt;/strong&gt; une fois le CMG et le crédit d'impôt déduits — l'écart réel est bien plus serré que les tarifs horaires bruts ne le laissent croire, surtout depuis la linéarisation du CMG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La garde à domicile reste la plus coûteuse&lt;/strong&gt; en solo ; en partagée, elle redevient compétitive, au prix d'une organisation à deux familles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Autrement dit : le classement par prix affiché et le classement par reste à charge réel ne sont pas le même classement. C'est exactement le piège que j'avais déjà vu en comparant des statuts freelance — le brut ment, le net décide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le tableau ne chiffre pas
&lt;/h2&gt;

&lt;p&gt;Et c'est là que, comme pour le freelance, l'argent cesse d'être le critère décisif. Plusieurs choses ne rentrent dans aucune cellule :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Les horaires.&lt;/strong&gt; Une crèche ferme à heure fixe et vous attend rarement ; une assistante maternelle offre une souplesse que les parents aux journées imprévisibles paient volontiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les maladies.&lt;/strong&gt; La collectivité, c'est des microbes à répétition les premiers mois, donc des absences — les vôtres, au travail. À l'inverse, une nounou est un &lt;em&gt;point de défaillance unique&lt;/em&gt; : le jour où &lt;em&gt;elle&lt;/em&gt; est malade, vous n'avez pas de solution de repli.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La socialisation&lt;/strong&gt; en collectivité contre le cocon d'un petit effectif : ni l'un ni l'autre n'est « mieux » dans l'absolu, ça dépend de l'enfant et de ce que vous cherchez.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La disponibilité.&lt;/strong&gt; Le plus beau tableau ne sert à rien si les crèches de votre secteur affichent complet — la place réelle, souvent, décide à votre place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  La méthode, plus que la réponse
&lt;/h2&gt;

&lt;p&gt;Je ne vais pas vous dire quel mode de garde choisir : comme pour freelance ou CDI, la réponse dépend de vos revenus, de vos horaires et de votre tolérance à l'imprévu. Ce qui se transpose, c'est la méthode. Calculez votre &lt;strong&gt;reste à charge net après aides&lt;/strong&gt; pour chaque option — prix affiché, moins CMG, moins crédit d'impôt — avec vos propres chiffres et les barèmes en vigueur. Puis, une fois l'écart d'argent réduit à sa vraie taille, tranchez sur le non-financier : horaires, robustesse, place disponible. En France, sur presque toutes ces décisions, le prix affiché n'est jamais le vrai nombre. Le vrai nombre est plus bas, plus intéressant, et il ne se révèle qu'à celui qui prend la peine de dérouler la cascade.&lt;/p&gt;

</description>
      <category>finances</category>
      <category>parentalité</category>
      <category>france</category>
      <category>aides</category>
    </item>
    <item>
      <title>One builder line and my rebuilds stopped being coffee breaks</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/one-builder-line-and-my-rebuilds-stopped-being-coffee-breaks-2634</link>
      <guid>https://dev.to/aelmufti/one-builder-line-and-my-rebuilds-stopped-being-coffee-breaks-2634</guid>
      <description>&lt;p&gt;There's a category of performance work where you profile, hypothesize, measure, and claw back milliseconds. And then there's the other kind: someone else rewrote the engine, and you just have to move over. The esbuild-based &lt;em&gt;application builder&lt;/em&gt; is the second kind. New Angular apps have used it by default since v17; the interesting story is migrating an old app that grew up on webpack.&lt;/p&gt;

&lt;p&gt;The headline from the app I migrated — five years old, thick with legacy — is blunt: cold production builds went from "start it and go do something else" to under a minute, and the dev server's incremental rebuilds dropped from double-digit seconds to fast enough that I stopped alt-tabbing. I did not optimize anything. I changed which tool builds the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration is one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ng update @angular/cli &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;use-application-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schematic rewrites &lt;code&gt;angular.json&lt;/code&gt; from &lt;code&gt;browser&lt;/code&gt; to &lt;code&gt;application&lt;/code&gt;, flips &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;browser&lt;/code&gt;, adjusts output paths, and leaves the rest of your configuration alone. Most of what you already declare transfers untouched: &lt;code&gt;fileReplacements&lt;/code&gt;, the &lt;code&gt;styles&lt;/code&gt; and &lt;code&gt;scripts&lt;/code&gt; arrays, &lt;code&gt;assets&lt;/code&gt;, size &lt;code&gt;budgets&lt;/code&gt;. If your app is plain — no custom builders, no webpack fiddling — there's a decent chance you run the schematic, run &lt;code&gt;ng serve&lt;/code&gt;, and you're done. Mine was not plain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Casualty #1: the custom webpack config
&lt;/h2&gt;

&lt;p&gt;If you're on &lt;code&gt;ngx-build-plus&lt;/code&gt; or &lt;code&gt;@angular-builders/custom-webpack&lt;/code&gt;, this is the part that stings: there is no "custom esbuild config" escape hatch of the same shape. Each thing your webpack config did needs its own esbuild-era answer. Going through mine turned out to be an archaeology dig — and most of the artifacts deserved burial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;DefinePlugin&lt;/code&gt; injecting build-time constants — replaced by the builder's &lt;code&gt;define&lt;/code&gt; option. One-to-one.&lt;/li&gt;
&lt;li&gt;A loader shim for a library that assumed Node globals — the library had shipped a browser build two majors ago. Deleted the shim, updated the import.&lt;/li&gt;
&lt;li&gt;A bundle-analyzer hook — &lt;code&gt;ng build --stats-json&lt;/code&gt; plus &lt;code&gt;esbuild-visualizer&lt;/code&gt; covers it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson generalizes: a years-old webpack config is mostly fossilized workarounds. Treat the migration as a chance to re-justify each line, not to port it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Casualty #2: CommonJS gets loud
&lt;/h2&gt;

&lt;p&gt;The new builder outputs ESM and is much less patient with CommonJS dependencies. Every CJS import now triggers a warning, and the old reflex — add it to &lt;code&gt;allowedCommonJsDependencies&lt;/code&gt; and move on — costs you real money here, because CJS modules defeat tree-shaking. The warnings are a to-do list, and working through it is where the bundle wins hide: swapping one dinosaur date library for its ESM successor cut more from my main bundle than any lazy-loading work I'd done that quarter. Relatedly: if any app code still calls &lt;code&gt;require()&lt;/code&gt; directly, it's over — output is ESM, imports only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get beyond speed
&lt;/h2&gt;

&lt;p&gt;The builder isn't just esbuild bolted on. Dev serve runs through Vite, which is where the instant rebuilds come from. SSR stopped being a parallel universe: the same &lt;code&gt;application&lt;/code&gt; builder handles the server bundle and prerendering via the &lt;code&gt;server&lt;/code&gt; and &lt;code&gt;prerender&lt;/code&gt; options, instead of the old Universal two-builder contraption. And build output is properly hashed and code-split without the tuning folklore webpack accumulated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order of operations that worked
&lt;/h2&gt;

&lt;p&gt;One process note, learned the annoying way: don't migrate the builder and update Angular majors in the same branch. First get the app green on a current Angular with the &lt;em&gt;old&lt;/em&gt; builder — that isolates framework breakage from build breakage. Then flip the builder, with zero other changes, so every new error is unambiguously the builder's. Then burn down the CommonJS warnings at leisure. Three boring PRs beat one heroic one.&lt;/p&gt;

&lt;p&gt;Verdict: this is the rare migration with an asymmetric payoff — a day or two of tooling archaeology, repaid every single time anyone on the team saves the file. If the only thing holding you back is a custom webpack config, that config is probably the strongest argument &lt;em&gt;for&lt;/em&gt; going.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>esbuild</category>
      <category>vite</category>
      <category>build</category>
    </item>
    <item>
      <title>My route changes crossfade now, and I wrote zero animation code</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/my-route-changes-crossfade-now-and-i-wrote-zero-animation-code-5efo</link>
      <guid>https://dev.to/aelmufti/my-route-changes-crossfade-now-and-i-wrote-zero-animation-code-5efo</guid>
      <description>&lt;p&gt;Page-transition animations in Angular used to mean the full &lt;code&gt;@angular/animations&lt;/code&gt; liturgy: a &lt;code&gt;trigger&lt;/code&gt; on the router outlet, &lt;code&gt;query(':enter')&lt;/code&gt; and &lt;code&gt;query(':leave')&lt;/code&gt; incantations, absolute-positioning both pages during the overlap so the layout doesn't jump, and a prayer. I shipped exactly one of those in my career, and I remember the CSS more than the feature.&lt;/p&gt;

&lt;p&gt;The browser can just do this now. The View Transitions API snapshots the old DOM, swaps in the new one, and animates between the two snapshots on the compositor. Angular's router plugs into it with one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;provideRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;withViewTransitions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole baseline. Every navigation is now wrapped in &lt;code&gt;document.startViewTransition()&lt;/code&gt;, and you get a quick crossfade between routes without touching a single component. In a browser without the API, the call is skipped and navigation behaves exactly as before — no fallback branch to write, no feature detection. It is progressive enhancement in the original sense of the term.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared-element trick
&lt;/h2&gt;

&lt;p&gt;The crossfade is nice; the morph is the party piece. Give an element on the outgoing page and an element on the incoming page the same &lt;code&gt;view-transition-name&lt;/code&gt;, and the browser animates one into the other — position, size, the lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* list page */&lt;/span&gt;
&lt;span class="nc"&gt;.product-card__image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-hero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* detail page */&lt;/span&gt;
&lt;span class="nc"&gt;.product-detail__hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-hero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click a card, and its thumbnail glides into place as the detail hero. This is the effect native apps have lorded over the web for a decade, and it's two lines of CSS. One rule to respect: a &lt;code&gt;view-transition-name&lt;/code&gt; must be unique on the page at transition time. For a list of cards that means assigning the name dynamically to the clicked card only — bind a style on click, or you'll get a console warning and no animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customizing without an animation library
&lt;/h2&gt;

&lt;p&gt;The snapshots are exposed as pseudo-elements, so the timing and easing are plain CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150ms&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-slide-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200ms&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-slide-in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slide-ins, directional wipes, staggered headers — it's keyframes on pseudo-elements, not a TypeScript DSL. The &lt;code&gt;root&lt;/code&gt; argument targets the whole page; named transitions get their own pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skipping when you should
&lt;/h2&gt;

&lt;p&gt;Two navigations deserve no theatrics: back/forward (users expect instant), and anything under &lt;code&gt;prefers-reduced-motion&lt;/code&gt;. The router hands you the hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;withViewTransitions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onViewTransitionCreated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reduced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-reduced-motion: reduce)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reduced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipTransition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;skipTransition()&lt;/code&gt; aborts the animation but not the navigation. Reduced motion isn't a nice-to-have flourish — vestibular users report actual nausea from full-page movement, so treat the media query as a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;First, support: Chromium and Safari have it; if a visitor's browser doesn't, they get an ordinary instant navigation — acceptable by design. Second, the API snapshots the page, and on a very long, very busy DOM that snapshot isn't free; if a route is heavy, a janky transition is worse than none, and &lt;code&gt;skipTransition()&lt;/code&gt; is your friend. Third, don't mix systems: router-outlet animations from &lt;code&gt;@angular/animations&lt;/code&gt; and view transitions both want to own the same moment, and they fight. I deleted the old trigger entirely. The browser compositor does this job better than a JavaScript animation engine ever did — and the code I got to remove was exactly the code I never wanted to maintain.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>router</category>
      <category>viewtransitions</category>
      <category>css</category>
    </item>
    <item>
      <title>hostDirectives ended my BaseComponent inheritance</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Fri, 26 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/hostdirectives-ended-my-basecomponent-inheritance-4o8j</link>
      <guid>https://dev.to/aelmufti/hostdirectives-ended-my-basecomponent-inheritance-4o8j</guid>
      <description>&lt;p&gt;Somewhere in every mature Angular codebase there is a &lt;code&gt;BaseButtonComponent&lt;/code&gt;. It started innocently — shared disabled handling, maybe a loading spinner. Then someone added analytics to it. Then keyboard handling. Then a &lt;code&gt;BaseIconButtonComponent extends BaseButtonComponent&lt;/code&gt;, and now there's a class four levels up the chain that nobody can modify without re-testing thirty components, because inheritance gives you everything your ancestors ever did, forever, whether you wanted it or not.&lt;/p&gt;

&lt;p&gt;The directive composition API is Angular saying: stop inheriting behaviors, &lt;em&gt;apply&lt;/em&gt; them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-save-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostDirectives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;DisabledStateDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tooltipText: hint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SaveButtonComponent&lt;/code&gt; now behaves as if &lt;code&gt;DisabledStateDirective&lt;/code&gt; and &lt;code&gt;TooltipDirective&lt;/code&gt; were written on its host element by every consumer, automatically. Each behavior lives in its own small, individually testable directive. No chain, no god-class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inputs and outputs are private by default
&lt;/h2&gt;

&lt;p&gt;This is the design decision I appreciate most. A host directive's inputs and outputs are &lt;em&gt;not&lt;/em&gt; exposed on the component unless you list them. In the example above, &lt;code&gt;DisabledStateDirective&lt;/code&gt; might have inputs of its own, but consumers of &lt;code&gt;app-save-button&lt;/code&gt; can't see them; the tooltip's &lt;code&gt;tooltipText&lt;/code&gt; input is exposed, renamed to &lt;code&gt;hint&lt;/code&gt;. Compare that to inheritance, where every &lt;code&gt;@Input&lt;/code&gt; up the chain leaks into your public API and removing one is a breaking change you don't discover until a template somewhere fails to compile. With composition, your component's surface is a deliberate allowlist.&lt;/p&gt;

&lt;h2&gt;
  
  
  They share the host's DI node
&lt;/h2&gt;

&lt;p&gt;Host directives are instantiated alongside the component, on the same element injector. Which means the component can just ask for them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DisabledStateDirective&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directive holds the state and the host-element bindings; the component reads it through DI. This is the same relationship a base class gave you via &lt;code&gt;this.isDisabled&lt;/code&gt;, except the dependency is explicit, mockable in a test, and swappable. It also works the other way — the directive can inject the host component when it genuinely needs to — though every time I've been tempted by that direction it was the design telling me the directive knew too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sharp edges
&lt;/h2&gt;

&lt;p&gt;Three constraints, all consequences of host directives being a &lt;em&gt;compile-time&lt;/em&gt; feature. The directives must be standalone — NgModule-declared ones don't qualify. Application is static: you cannot conditionally attach a host directive at runtime, so if a behavior should sometimes be inert, put the condition &lt;em&gt;inside&lt;/em&gt; the directive (an input that gates its listeners) rather than trying to gate its application. And the same directive reaching one host via two paths — say, directly and through another host directive that also applies it — is a compile error, not a silent double application.&lt;/p&gt;

&lt;p&gt;Ordering is worth knowing too: host directives' lifecycle hooks run &lt;em&gt;before&lt;/em&gt; the host component's own, in the order they're listed. Their host bindings are applied first as well, which lets the component win when both write the same attribute — usually what you want, occasionally a surprise when it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it changed my defaults
&lt;/h2&gt;

&lt;p&gt;The pattern that sold me was aria wiring. We had accessibility logic — &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;aria-disabled&lt;/code&gt;, keyboard activation on Space/Enter — copy-pasted across a dozen interactive components because putting it in a base class would have forced unrelated widgets into one hierarchy. It's one directive now. Each widget adds it to &lt;code&gt;hostDirectives&lt;/code&gt;, exposes nothing, and the behavior has one home and one test file. Inheritance answers "what is this component?" with an ancestry; composition answers with a parts list. For UI behaviors, the parts list is almost always the truthful answer.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>directives</category>
      <category>composition</category>
      <category>architecture</category>
    </item>
    <item>
      <title>My dashboard renders components it has never met</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/my-dashboard-renders-components-it-has-never-met-4ij0</link>
      <guid>https://dev.to/aelmufti/my-dashboard-renders-components-it-has-never-met-4ij0</guid>
      <description>&lt;p&gt;The requirement sounds exotic until the day it lands on your sprint: the server sends a JSON layout — a list of widget descriptors — and the app has to render whatever comes back. A chart here, a KPI tile there, a table if the user configured one. The template can't &lt;code&gt;@if&lt;/code&gt; its way through this, because the template doesn't know the shape. The component type itself is data.&lt;/p&gt;

&lt;p&gt;If your memory of dynamic components involves &lt;code&gt;ComponentFactoryResolver&lt;/code&gt;, entry components, and a ceremony that felt like summoning something, good news: all of that is gone. The factory resolver was removed back in v13. What's left is almost embarrassingly small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;viewChild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ViewContainerRef&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;renderWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WidgetDescriptor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WIDGETS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createComponent&lt;/code&gt; takes the component class. Directly. The registry &lt;code&gt;WIDGETS&lt;/code&gt; is a plain object mapping descriptor types to imported classes. That's the entire trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  setInput is not optional
&lt;/h2&gt;

&lt;p&gt;The mistake that costs an afternoon: setting inputs by assigning to the instance. &lt;code&gt;ref.instance.config = descriptor.config&lt;/code&gt; compiles, appears to work in a demo, and then betrays you twice in production. Under &lt;code&gt;OnPush&lt;/code&gt;, a bare property assignment doesn't mark the component for check, so the widget renders stale. And if the widget uses signal inputs — &lt;code&gt;config = input.required&amp;lt;WidgetConfig&amp;gt;()&lt;/code&gt; — the instance property isn't even writable that way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ref.setInput('config', value)&lt;/code&gt; is the API that goes through the front door: it sets the input the same way a template binding would, marks for check, and works identically for decorator inputs and signal inputs. It's also what makes re-rendering on config change trivial — call it again with the new value instead of destroying and recreating the widget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outputs and cleanup
&lt;/h2&gt;

&lt;p&gt;Outputs are just there on the instance. With the modern &lt;code&gt;output()&lt;/code&gt; function they expose &lt;code&gt;subscribe&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ChartWidget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pointSelected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleanup has one rule worth memorizing: a component created into a &lt;code&gt;ViewContainerRef&lt;/code&gt; is part of that view tree, so when the container's host dies, your dynamic components die with it. You only call &lt;code&gt;ref.destroy()&lt;/code&gt; yourself when a widget is removed &lt;em&gt;while the dashboard lives on&lt;/em&gt; — user deletes a tile, layout reloads. Forgetting that case leaks the component &lt;em&gt;and&lt;/em&gt; its subscriptions; I register the output subscription with &lt;code&gt;DestroyRef&lt;/code&gt; or tie it to the ref's own destruction so the two can't drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  NgComponentOutlet, for when the template should decide
&lt;/h2&gt;

&lt;p&gt;A surprising number of "dynamic component" cases don't need any of the imperative machinery. If you just need &lt;em&gt;render this type with these inputs&lt;/em&gt; in a template position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@for (w of widgets(); track w.id) {
  &lt;span class="nt"&gt;&amp;lt;ng-container&lt;/span&gt;
    &lt;span class="na"&gt;[ngComponentOutlet]=&lt;/span&gt;&lt;span class="s"&gt;"registry[w.type]"&lt;/span&gt;
    &lt;span class="na"&gt;[ngComponentOutletInputs]=&lt;/span&gt;&lt;span class="s"&gt;"w.config"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ngComponentOutletInputs&lt;/code&gt; (since v16.2) does the &lt;code&gt;setInput&lt;/code&gt; dance for you, including diffing the object between checks. No &lt;code&gt;ViewContainerRef&lt;/code&gt;, no refs to track, no manual destroy — the &lt;code&gt;@for&lt;/code&gt; owns the lifecycle. I reach for the imperative API only when I need the &lt;code&gt;ComponentRef&lt;/code&gt; itself: subscribing to outputs, calling methods on the instance, moving it between containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to do any of this
&lt;/h2&gt;

&lt;p&gt;The boundary that keeps a codebase sane: dynamic instantiation is for when the component type &lt;em&gt;arrives as data&lt;/em&gt;. If the set of possibilities is known at build time and you're choosing based on state, a &lt;code&gt;@switch&lt;/code&gt; over the cases — or &lt;code&gt;@defer&lt;/code&gt; if the goal is loading less upfront — is simpler, type-checked against actual inputs, and friendlier to the optimizer. A registry object holding component classes makes every one of them eagerly-bundled and invisible to template type checking; &lt;code&gt;setInput('confg', ...)&lt;/code&gt; is a runtime error, not a compile error. I've seen a plugin architecture built where three &lt;code&gt;@if&lt;/code&gt; branches would have done. The JSON-driven dashboard genuinely needs this machinery. Most screens don't — and now that the machinery is this small, the temptation to use it everywhere is the real danger.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>components</category>
      <category>dynamic</category>
      <category>architecture</category>
    </item>
    <item>
      <title>My star-rating widget is a real form control now</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/my-star-rating-widget-is-a-real-form-control-now-5dd0</link>
      <guid>https://dev.to/aelmufti/my-star-rating-widget-is-a-real-form-control-now-5dd0</guid>
      <description>&lt;p&gt;Every custom input widget reaches the same fork in the road. The team builds a star rating, or a tag picker, or a duration field. It works. Then someone tries to drop it into an existing reactive form — &lt;code&gt;formControlName="rating"&lt;/code&gt; — and it does nothing, so they "solve" it with a &lt;code&gt;value&lt;/code&gt; input, a &lt;code&gt;valueChange&lt;/code&gt; output, and a subscription gluing the two to the form in the parent. Multiply by every form the widget appears in, and validators, dirty tracking and disabling all live somewhere else, reimplemented, badly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ControlValueAccessor&lt;/code&gt; is the actual answer: the interface Angular's own &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; directives implement behind the scenes. Implement it, and your component plugs into &lt;code&gt;formControlName&lt;/code&gt;, &lt;code&gt;formControl&lt;/code&gt; and &lt;code&gt;ngModel&lt;/code&gt; like it was born there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract is four methods
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-star-rating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NG_VALUE_ACCESSOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;useExisting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;forwardRef&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;StarRatingComponent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StarRatingComponent&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;ControlValueAccessor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;onTouched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="nf"&gt;writeValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;registerOnChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;registerOnTouched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onTouched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;setDisabledState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onTouched&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as two directions of traffic. &lt;code&gt;writeValue&lt;/code&gt; is the form talking to you: initial value, &lt;code&gt;setValue&lt;/code&gt;, &lt;code&gt;reset&lt;/code&gt;. The two registered callbacks are you talking to the form: &lt;code&gt;onChange&lt;/code&gt; when the user changes the value, &lt;code&gt;onTouched&lt;/code&gt; when they've interacted enough that blur-style validation should wake up. The &lt;code&gt;NG_VALUE_ACCESSOR&lt;/code&gt; provider — &lt;code&gt;multi: true&lt;/code&gt;, &lt;code&gt;forwardRef&lt;/code&gt; because the class isn't defined yet at decorator evaluation — is how the &lt;code&gt;formControlName&lt;/code&gt; directive sitting on your tag finds you. Internal state as a signal is my one modernization: the template gets clean reactive reads, and nothing about the CVA contract objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three bugs everyone ships
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Silent touched state.&lt;/strong&gt; Forgetting &lt;code&gt;onTouched&lt;/code&gt; is invisible in the demo and obvious in production: any error message gated on &lt;code&gt;control.touched&lt;/code&gt; — which is the convention almost every form uses — never appears for your widget. Call it on blur, or on first interaction if your control has no meaningful blur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring setDisabledState.&lt;/strong&gt; &lt;code&gt;form.disable()&lt;/code&gt; reaches your component exclusively through this method. Skip it and your widget stays merrily clickable inside a disabled form — worse than a compile error because nobody tests disabling until a submit button starts double-firing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The echo loop.&lt;/strong&gt; &lt;code&gt;writeValue&lt;/code&gt; must update internal state &lt;em&gt;silently&lt;/em&gt;. If it calls &lt;code&gt;onChange&lt;/code&gt; — directly, or indirectly because your setter fires the same path user interaction does — then every programmatic &lt;code&gt;setValue&lt;/code&gt; bounces back into the form model. Best case it re-runs validators and marks the form dirty from a non-user change; worst case, with value normalization in the middle, it ping-pongs forever. Keep the paths separate: &lt;code&gt;writeValue&lt;/code&gt; writes the signal; &lt;code&gt;rate()&lt;/code&gt; writes the signal &lt;em&gt;and&lt;/em&gt; reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  When CVA is the wrong tool
&lt;/h2&gt;

&lt;p&gt;The ceremony earns its keep when the component owns a genuinely non-input interaction model — stars, chips, a canvas signature pad. If your "custom control" is a styled wrapper around native inputs (a labeled input with an icon, an address group), you don't need a value accessor; pass the &lt;code&gt;FormControl&lt;/code&gt; in as an input, or reach for &lt;code&gt;ControlContainer&lt;/code&gt; injection so child and parent share the form group. I've reviewed CVAs wrapping a single &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; that re-forwarded every event by hand — forty lines that replaced zero.&lt;/p&gt;

&lt;p&gt;But when it fits, it's one of Angular's best contracts: the day our star rating became a real control, three components deleted their glue subscriptions, validation messages started appearing on blur like everywhere else, and the widget stopped being special. That's the goal — not a clever component, an unremarkable one.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>forms</category>
      <category>controlvalueaccessor</category>
      <category>signals</category>
    </item>
    <item>
      <title>disabled finally means disabled: input transforms</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Fri, 19 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/disabled-finally-means-disabled-input-transforms-2m3j</link>
      <guid>https://dev.to/aelmufti/disabled-finally-means-disabled-input-transforms-2m3j</guid>
      <description>&lt;p&gt;Here's a bug that has existed in every Angular design system I've touched. You build a button with a boolean input, someone uses it the way HTML has trained them to for thirty years:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/app-button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the button stays clickable. Because without brackets that's not a boolean binding — it's a static attribute, and static attributes bind the &lt;em&gt;string&lt;/em&gt; &lt;code&gt;""&lt;/code&gt;. Even better: &lt;code&gt;disabled="false"&lt;/code&gt; passes the string &lt;code&gt;"false"&lt;/code&gt;, which is truthy, so the button that was told &lt;em&gt;false&lt;/em&gt; renders disabled. The type system said &lt;code&gt;boolean&lt;/code&gt;; the DOM had other plans.&lt;/p&gt;

&lt;p&gt;For years the workaround was the coercion ritual — a getter/setter pair with &lt;code&gt;coerceBooleanProperty&lt;/code&gt; from the CDK on every single boolean input, eight lines of ceremony per flag, so pervasive that the CDK shipped &lt;code&gt;BooleanInput&lt;/code&gt; types just to annotate the pattern. The framework finally absorbed the ritual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ButtonComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;booleanAttribute&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;tabIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;numberAttribute&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transform is a function that runs on every value the input receives, before it lands. &lt;code&gt;booleanAttribute&lt;/code&gt; implements the HTML attribute semantics you actually wanted: absent is &lt;code&gt;false&lt;/code&gt;, present-but-empty is &lt;code&gt;true&lt;/code&gt;, and real booleans pass through. &lt;code&gt;numberAttribute&lt;/code&gt; does the string → number conversion, so &lt;code&gt;tabindex="3"&lt;/code&gt; from a template arrives as the number &lt;code&gt;3&lt;/code&gt;. Both work identically on decorator inputs — &lt;code&gt;@Input({ transform: booleanAttribute })&lt;/code&gt; — if you're not on signal inputs yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The type-level trick
&lt;/h2&gt;

&lt;p&gt;The clever part is that a transform splits the input's type in two. The template side may now pass &lt;code&gt;string | boolean&lt;/code&gt; — the template type checker knows about the transform and accepts &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;disabled="false"&lt;/code&gt; and &lt;code&gt;[disabled]="expr"&lt;/code&gt; alike. The class side reads a clean &lt;code&gt;boolean&lt;/code&gt;, always, because everything funnels through the coercion. Write-type and read-type diverge, and both are checked. That's the exact contract the getter/setter pattern was hand-rolling, now expressed in one option and visible to the compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom transforms
&lt;/h2&gt;

&lt;p&gt;The built-ins cover the attribute cases; the option takes any function of the right shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchBoxComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalizing whitespace, clamping a number into a range, defaulting null to a sentinel — anything that answers "what shape should this value have once it's mine." The constraint to respect: the transform must be a standalone, statically analyzable function. A top-level function or a &lt;code&gt;static&lt;/code&gt; method works; an inline arrow that closes over &lt;code&gt;this&lt;/code&gt; does not, and the compiler will tell you so. Transforms also run on &lt;em&gt;every&lt;/em&gt; write, including &lt;code&gt;setInput&lt;/code&gt; from dynamic-component code, so keep them cheap and pure — a transform with side effects is an input that changes behavior depending on how often change detection looked at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalization, not validation
&lt;/h2&gt;

&lt;p&gt;The temptation once you have a function hook is to validate in it — throw on out-of-range, reject bad enum strings. Resist. A throw inside a transform surfaces as a change-detection error pointing vaguely at the parent template, which is miserable error locality for the consumer. Transforms are for making sloppy-but-well-meaning input shapes correct (&lt;code&gt;"3"&lt;/code&gt; → &lt;code&gt;3&lt;/code&gt;); actual invariants belong where failures can be reported properly — a &lt;code&gt;computed()&lt;/code&gt; that clamps, an &lt;code&gt;effect&lt;/code&gt; that warns in dev mode, a form validator.&lt;/p&gt;

&lt;p&gt;It's a small feature. It deleted more lines from our component library than any small feature has a right to — and it closed the gap between how our components claimed to be used and how HTML users were always going to use them. &lt;code&gt;&amp;lt;app-button disabled&amp;gt;&lt;/code&gt; does what it says now. It only took a decade.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>inputs</category>
      <category>signals</category>
      <category>components</category>
    </item>
    <item>
      <title>I deleted my CanActivate classes and my routing got readable</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Wed, 17 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/i-deleted-my-canactivate-classes-and-my-routing-got-readable-ald</link>
      <guid>https://dev.to/aelmufti/i-deleted-my-canactivate-classes-and-my-routing-got-readable-ald</guid>
      <description>&lt;p&gt;For years a route guard meant a class: &lt;code&gt;@Injectable&lt;/code&gt;, &lt;code&gt;implements CanActivate&lt;/code&gt;, a constructor full of services, and an entry in some &lt;code&gt;providers&lt;/code&gt; array you'd inevitably forget. The guard was usually four lines of actual logic wrapped in twenty lines of ceremony. Functional guards deleted the wrapper, and the same &lt;code&gt;inject()&lt;/code&gt; move that cleaned up interceptors did it again here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authGuard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CanActivateFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUrlTree&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;queryParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No class, no provider registration — you reference the function directly in the route. But the syntax win is the boring part. The interesting part is that going functional made the three jobs guards do finally feel like three different tools instead of one overloaded interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return a UrlTree, not false
&lt;/h2&gt;

&lt;p&gt;The most common guard bug I find in review isn't a logic error — it's a guard that returns &lt;code&gt;false&lt;/code&gt;. Returning &lt;code&gt;false&lt;/code&gt; cancels the navigation and leaves the user exactly where they were, with no explanation. They clicked a link, nothing happened, and now they think the app is broken. Half the time they're sitting on a blank shell because the click came from a fresh page load.&lt;/p&gt;

&lt;p&gt;Returning a &lt;code&gt;UrlTree&lt;/code&gt; is almost always what you meant: it cancels the original navigation &lt;em&gt;and&lt;/em&gt; redirects in one step. Blocked from a page → send them to &lt;code&gt;/login&lt;/code&gt; with a &lt;code&gt;returnUrl&lt;/code&gt; so you can bounce them back after they sign in. The rule I use: a guard should either say yes, or say where to go instead. &lt;code&gt;false&lt;/code&gt; is for the rare case where staying put with a toast is genuinely the right UX, and even then I'd rather show the toast and redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  canActivate vs canMatch — they answer different questions
&lt;/h2&gt;

&lt;p&gt;These get used interchangeably and they shouldn't be. &lt;code&gt;canActivate&lt;/code&gt; asks "this route matched the URL — is the user allowed to land on it?" It runs &lt;em&gt;after&lt;/em&gt; the router has picked the route. &lt;code&gt;canMatch&lt;/code&gt; asks the earlier question: "does this route even apply?" It runs during route matching, before the component is chosen.&lt;/p&gt;

&lt;p&gt;That timing difference is the whole point. With &lt;code&gt;canMatch&lt;/code&gt; you can have two routes for the same path and let the guard decide which one wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminDashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;canMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserDashboard&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admins match the first route, everyone else falls through to the second. You can't express that with &lt;code&gt;canActivate&lt;/code&gt; — by the time it runs, the route is already locked in. The other &lt;code&gt;canMatch&lt;/code&gt; superpower: on a lazy route, a failed &lt;code&gt;canMatch&lt;/code&gt; means the chunk never even downloads. Guarding a whole admin feature behind &lt;code&gt;canActivate&lt;/code&gt; still ships its JavaScript to anonymous visitors; &lt;code&gt;canMatch&lt;/code&gt; keeps it on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small guards compose; god-guards don't
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;canActivate&lt;/code&gt; takes an array, I stopped writing guards that check three things and started writing three guards that check one thing each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Billing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasBillingRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgIsActive&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They run in order and the first one to return a &lt;code&gt;UrlTree&lt;/code&gt; wins, so the redirects naturally prioritize: not logged in beats wrong role beats suspended org. Each guard is independently testable and reusable across routes. A single guard doing all three reads fine the day you write it and becomes a tangle of nested conditions the third time someone adds a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolvers: powerful, and the easiest way to make your app feel frozen
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ResolveFn&lt;/code&gt; got the same functional treatment, and it's genuinely useful — but it's the guard-family tool I reach for least, on purpose. A resolver fetches data &lt;em&gt;before&lt;/em&gt; the route activates, so the component renders with everything already present. No loading spinner inside the page, no flash of empty state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderResolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolveFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paramMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch is in that word "before." Navigation &lt;em&gt;blocks&lt;/em&gt; on the resolver. If the API takes two seconds, clicking the link does nothing visible for two seconds — the user is stuck on the previous page with no feedback. You've moved the loading state from "a spinner on the new page" to "the entire app appears to hang." If you use resolvers, you almost have to wire a progress bar to the router's &lt;code&gt;NavigationStart&lt;/code&gt;/&lt;code&gt;NavigationEnd&lt;/code&gt; events, or the UX is worse than just fetching in the component.&lt;/p&gt;

&lt;p&gt;So my line is: a resolver earns its place when the page is genuinely meaningless without the data — an order detail page with no order isn't a page, it's a 404 waiting to happen, and resolving lets you redirect on a missing record before anything renders. For everything else, fetching in the component with &lt;code&gt;httpResource&lt;/code&gt; gives you the same data with an honest, local loading state and no global freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Deciding whether the route applies, or want to skip downloading a lazy chunk → &lt;code&gt;canMatch&lt;/code&gt;. Deciding whether this user may enter a route that does apply → &lt;code&gt;canActivate&lt;/code&gt;, returning a &lt;code&gt;UrlTree&lt;/code&gt; when the answer is no. The page is structurally meaningless without server data, and you've got a progress indicator → &lt;code&gt;ResolveFn&lt;/code&gt;. Otherwise, let the component fetch its own data. All three are just functions now, which means all three are just unit tests now too.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>routing</category>
      <category>guards</category>
      <category>resolvers</category>
    </item>
    <item>
      <title>My route params bind straight to inputs now</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/my-route-params-bind-straight-to-inputs-now-2033</link>
      <guid>https://dev.to/aelmufti/my-route-params-bind-straight-to-inputs-now-2033</guid>
      <description>&lt;p&gt;Here's a pattern every Angular dev has typed a hundred times: a component needs the &lt;code&gt;:id&lt;/code&gt; from the URL, so you inject &lt;code&gt;ActivatedRoute&lt;/code&gt;, subscribe to &lt;code&gt;paramMap&lt;/code&gt;, pull the value, remember to unsubscribe, and handle the fact that the component is reused when only the id changes so &lt;code&gt;ngOnInit&lt;/code&gt; won't fire twice. Six lines of plumbing to read one string off the URL.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withComponentInputBinding()&lt;/code&gt; deletes all of it. You turn it on once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;bootstrapApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;provideRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;withComponentInputBinding&lt;/span&gt;&lt;span class="p"&gt;())],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and now the router sets your component's inputs from the route, matched by name. A route param &lt;code&gt;:id&lt;/code&gt; binds to an input called &lt;code&gt;id&lt;/code&gt;. That's the entire mental model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- comes straight from :id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four sources feed the same inputs
&lt;/h2&gt;

&lt;p&gt;It's not just path params. The router binds from four places, and once you know all four you stop reaching for &lt;code&gt;ActivatedRoute&lt;/code&gt; almost entirely: resolved data and static &lt;code&gt;data&lt;/code&gt; on the route, path params (&lt;code&gt;:id&lt;/code&gt;), matrix params, and query params (&lt;code&gt;?tab=invoices&lt;/code&gt;). 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;data&lt;/code&gt; wins, then path params, then query params. The practical consequence: don't name a query param the same thing as something in your resolver &lt;code&gt;data&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real payoff is signal inputs
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// re-runs automatically when the URL id changes — no subscription,&lt;/span&gt;
&lt;span class="c1"&gt;// no ngOnChanges, no manual refetch&lt;/span&gt;
&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;httpResource&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/api/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Where it stops, and that's fine
&lt;/h2&gt;

&lt;p&gt;Two honest limits. First, it binds to the &lt;em&gt;routed&lt;/em&gt; component — the one named in the route config. A deeply nested child that wants the parent route's params still reads &lt;code&gt;ActivatedRoute&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;I treat &lt;code&gt;ActivatedRoute&lt;/code&gt; the way I now treat &lt;code&gt;ngOnDestroy&lt;/code&gt;: 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.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>routing</category>
      <category>signals</category>
      <category>inputs</category>
    </item>
    <item>
      <title>SignalStore is the first NgRx I'd actually reach for</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/signalstore-is-the-first-ngrx-id-actually-reach-for-58p9</link>
      <guid>https://dev.to/aelmufti/signalstore-is-the-first-ngrx-id-actually-reach-for-58p9</guid>
      <description>&lt;p&gt;I've spent a good chunk of my career talking teams &lt;em&gt;out&lt;/em&gt; of NgRx. Not because the Redux pattern is wrong — because most apps that adopted it didn't have a Redux-shaped problem. They had a "two components need the same list" problem, and they paid for it with an action, a reducer case, an effect, a selector, and three files, per feature. The ratio of ceremony to value was brutal.&lt;/p&gt;

&lt;p&gt;SignalStore is the first time the NgRx name has shown up in my projects without me wincing. It throws out the dispatch/reducer machinery and builds on signals directly. State is signals you read; methods are functions that change them. That's the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CartStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;withState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nf"&gt;withComputed&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="nf"&gt;withMethods&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;count&lt;/code&gt; and &lt;code&gt;total&lt;/code&gt; are signals. &lt;code&gt;add&lt;/code&gt; and &lt;code&gt;clear&lt;/code&gt; are methods. A component injects the store and reads &lt;code&gt;store.count()&lt;/code&gt; in its template like any other signal. No &lt;code&gt;store.select(...)&lt;/code&gt;, no string action types, no switch statement. The thing that used to be five concepts is now two: &lt;code&gt;withState&lt;/code&gt; and &lt;code&gt;withMethods&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  patchState, and the immutability you can't skip
&lt;/h2&gt;

&lt;p&gt;You never assign to state directly — you call &lt;code&gt;patchState&lt;/code&gt; with a partial update or an updater function. And the array spread in &lt;code&gt;add&lt;/code&gt; above isn't optional styling: SignalStore state follows the same rule signals always do. Mutating the existing array in place (&lt;code&gt;items().push(item)&lt;/code&gt;) changes the contents without changing the reference, so the signal never fires and your computed &lt;code&gt;count&lt;/code&gt; goes stale. Treat state as immutable, produce new references, and the reactive graph stays honest. This is the one rule that trips people coming from the mutate-anything world.&lt;/p&gt;

&lt;h2&gt;
  
  
  rxMethod is where the async lives
&lt;/h2&gt;

&lt;p&gt;Real stores fetch. &lt;code&gt;rxMethod&lt;/code&gt; is the bridge to RxJS for exactly that — it takes a stream of inputs and lets you run an Observable pipeline per emission, with proper cancellation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;loadItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rxMethod&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cartId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;tapResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;switchMap&lt;/code&gt; there is doing the work an NgRx effect used to: a second call cancels the first in-flight request, so a fast-typing user doesn't race two responses into your state out of order. This is the spot where the "signals killed RxJS" crowd gets corrected — streams are still the right tool for events over time, and SignalStore is happy to let them feed the signals. It's the same boundary as everywhere else in modern Angular: signals hold state, streams describe the async.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that actually matters: where you provide it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;providedIn: "root"&lt;/code&gt; gives you one global instance — right for genuinely app-wide state like the cart or the current user. But you can also leave that out and list the store in a component's &lt;code&gt;providers&lt;/code&gt; array. Now you get a fresh instance scoped to that component and its children, created when the feature mounts and &lt;em&gt;destroyed when it unmounts&lt;/em&gt;. State that belongs to one screen lives and dies with that screen, no manual reset.&lt;/p&gt;

&lt;p&gt;This is the call I see teams get wrong most often. They make every store global, then write cleanup code to wipe stale feature state on every navigation — reinventing component scope by hand. If the state is "this page's state," scope it to the page. If it's "the app's state," put it in root. &lt;code&gt;withEntities&lt;/code&gt; is worth knowing for the collection cases — it gives you normalized add/update/remove over an entity map so you're not hand-rolling the same dictionary logic again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you even need it?
&lt;/h2&gt;

&lt;p&gt;Honest answer: often not. A plain &lt;code&gt;@Injectable&lt;/code&gt; service holding a few &lt;code&gt;signal()&lt;/code&gt;s and exposing &lt;code&gt;computed()&lt;/code&gt; getters covers a surprising amount of shared state, and it's the lighter choice for a single feature with simple needs. SignalStore earns its dependency when you want &lt;em&gt;conventions&lt;/em&gt; a team will follow without a meeting — a predictable shape for every store, async that's already cancellation-safe, and &lt;code&gt;withEntities&lt;/code&gt;/custom &lt;code&gt;withX&lt;/code&gt; features you can compose and reuse across stores.&lt;/p&gt;

&lt;p&gt;The way I'd put it: plain signals service is the answer until you find yourself reinventing the same store skeleton for the fourth time. At that point SignalStore is the skeleton, already written, and unlike the NgRx of five years ago it doesn't make you pay for a Redux you don't have.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>ngrx</category>
      <category>signals</category>
      <category>state</category>
    </item>
  </channel>
</rss>
