<?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: Amanulla Khan</title>
    <description>The latest articles on DEV Community by Amanulla Khan (@amankhan55).</description>
    <link>https://dev.to/amankhan55</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%2F4025311%2F0c59a89d-ca77-4b42-ab52-c6f978fb6039.jpeg</url>
      <title>DEV Community: Amanulla Khan</title>
      <link>https://dev.to/amankhan55</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amankhan55"/>
    <language>en</language>
    <item>
      <title>9 Mistakes Teams Keep Making When Migrating Legacy Angular Apps (2026 Edition)</title>
      <dc:creator>Amanulla Khan</dc:creator>
      <pubDate>Tue, 28 Jul 2026 18:49:16 +0000</pubDate>
      <link>https://dev.to/amankhan55/9-mistakes-teams-keep-making-when-migrating-legacy-angular-apps-2026-edition-4349</link>
      <guid>https://dev.to/amankhan55/9-mistakes-teams-keep-making-when-migrating-legacy-angular-apps-2026-edition-4349</guid>
      <description>&lt;p&gt;Angular has changed more in the last three years than in the five before that. Standalone components have been the default for new Angular applications since v19, built-in control flow (&lt;code&gt;@if&lt;/code&gt;/&lt;code&gt;@for&lt;/code&gt;) has been stable since v17, signals are now a core part of Angular's modern reactivity model, zoneless change detection is the default for new apps in v21+, and as of v22 &lt;code&gt;OnPush&lt;/code&gt; is the default change detection strategy. If your app was last touched seriously back in the NgModule-and-&lt;code&gt;*ngIf&lt;/code&gt; era — or worse, it's still a hybrid AngularJS app — the migration path is no longer "run &lt;code&gt;ng update&lt;/code&gt; and fix whatever breaks."&lt;/p&gt;

&lt;p&gt;A note on how to read this: where a claim rests on specific framework behavior I've linked the primary source — an Angular RFC, a filed issue, or the official docs — rather than asking you to take my word for it, because a couple of these contradict advice that's currently circulating.&lt;/p&gt;

&lt;p&gt;Below are the mistakes I keep seeing on real migration projects, roughly in the order they bite you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treating the migration as a single "big bang" project
&lt;/h2&gt;

&lt;p&gt;The most expensive mistake happens before anyone writes code: deciding to freeze feature work for months and rewrite everything at once.&lt;/p&gt;

&lt;p&gt;Angular's own tooling assumes the opposite. &lt;code&gt;ng update&lt;/code&gt; enforces &lt;strong&gt;one major version at a time&lt;/strong&gt; — you cannot jump from v14 to v22 directly. Each hop (14→15→16→17→18→19→20→21→22) runs its own schematics. This isn't a style preference, it's enforced: Angular's release documentation states you can &lt;code&gt;ng update&lt;/code&gt; to any version "provided that... the version you want to update &lt;em&gt;from&lt;/em&gt; is within one major version of the version you want to upgrade to" — with a worked example of going 10 → 11 → 12 rather than 10 → 12 directly. A "big bang" plan collides with this immediately: teams try to jump straight to the latest major, &lt;code&gt;ng update&lt;/code&gt; refuses or gets &lt;code&gt;--force&lt;/code&gt;'d into a broken state, and someone spends a week bisecting which of six simultaneous major upgrades caused the regression.&lt;/p&gt;

&lt;p&gt;The fix is the strangler pattern applied to versions, not just code: upgrade one major version, run the automated migrations for that version, ship it, then move to the next. The cost of deferral isn't linear either — breaking changes compound, so a team that skipped five years of upgrades isn't facing five times the work of a team that skipped one.&lt;/p&gt;

&lt;p&gt;For a genuinely legacy (AngularJS 1.x) app, the equivalent mistake is trying to rewrite the whole SPA before shipping anything. The usual advice is a &lt;strong&gt;hybrid app via &lt;code&gt;UpgradeModule&lt;/code&gt;&lt;/strong&gt;, migrating route-by-route behind a shared shell. That still works — but go in knowing something the guides rarely mention: &lt;strong&gt;ngUpgrade and zoneless don't currently mix.&lt;/strong&gt; A team that tried it on Angular 19 + AngularJS 1.8 found change detection simply didn't fire for downgraded components, and an Angular team member investigating replied that "the upgrade module appears to depend on ZoneJS," pointing at specific code, while adding that enabling zoneless support "likely carries some amount of risk since there's so little usage left of the upgrade adapter that it's effectively untested" (&lt;a href="https://github.com/angular/angular/issues/61640" rel="noopener noreferrer"&gt;angular#61640&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I'd read that as "unsupported and untested," not "architecturally impossible" — nobody has declared it permanently off the table. But for planning purposes the distinction doesn't help you much: an untested path through a low-usage compatibility layer is not where you want your migration's critical path. The practical takeaway is that the hybrid phase and zoneless are sequential, not concurrent, and that second quote is the real signal — ngUpgrade is in maintenance territory. The bridge is one you need to actually finish crossing, not a steady state to park in for three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Running the standalone/control-flow schematics and assuming they're correct
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ng generate @angular/core:standalone&lt;/code&gt; and &lt;code&gt;ng generate @angular/core:control-flow&lt;/code&gt; are genuinely good tools — but they're pattern-matching schematics, not compilers with full semantic understanding of your templates. Mishandled &lt;code&gt;ng-template&lt;/code&gt; removal is a whole &lt;em&gt;genre&lt;/em&gt; of filed bug, not a one-off: &lt;a href="https://github.com/angular/angular/issues/52513" rel="noopener noreferrer"&gt;#52513&lt;/a&gt;, &lt;a href="https://github.com/angular/angular/issues/53288" rel="noopener noreferrer"&gt;#53288&lt;/a&gt;, &lt;a href="https://github.com/angular/angular/issues/53362" rel="noopener noreferrer"&gt;#53362&lt;/a&gt;, &lt;a href="https://github.com/angular/angular/issues/53383" rel="noopener noreferrer"&gt;#53383&lt;/a&gt;, &lt;a href="https://github.com/angular/angular/issues/59919" rel="noopener noreferrer"&gt;#59919&lt;/a&gt;, and &lt;a href="https://github.com/angular/angular/issues/64741" rel="noopener noreferrer"&gt;#64741&lt;/a&gt; are all variations on the schematic deleting or mangling a template that was still needed.&lt;/p&gt;

&lt;p&gt;Most of those specific reports are now fixed, and that's the point rather than a caveat: the same failure mode kept resurfacing in new shapes across several years of releases, because "is this template still referenced?" is a genuinely hard question to answer from pattern matching. Your codebase's particular shape is not guaranteed to be one of the ones somebody already filed.&lt;/p&gt;

&lt;p&gt;The nastiest variant is when the template is referenced from &lt;strong&gt;TypeScript&lt;/strong&gt; rather than another template, because nothing in the converted markup looks wrong:&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="c1"&gt;// Before migration&lt;/span&gt;
&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
  &amp;lt;div *ngIf="true; else someTpl"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;ng-template #someTpl /&amp;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;CountComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;someTpl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;viewChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;someTpl&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schematic converts the &lt;code&gt;*ngIf&lt;/code&gt; to &lt;code&gt;@if&lt;/code&gt;/&lt;code&gt;@else&lt;/code&gt; and removes the &lt;code&gt;&amp;lt;ng-template #someTpl /&amp;gt;&lt;/code&gt; — leaving the &lt;code&gt;viewChild&lt;/code&gt; query pointing at a template reference that no longer exists (&lt;a href="https://github.com/angular/angular/issues/59919" rel="noopener noreferrer"&gt;#59919&lt;/a&gt;). The template compiles; the query silently returns nothing at runtime; whatever dynamic rendering depended on that &lt;code&gt;ViewContainerRef&lt;/code&gt; quietly stops working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to actually do:&lt;/strong&gt; the schematic's own README says to verify the application between each step, and it accepts a &lt;code&gt;path&lt;/code&gt; option specifically so large apps can migrate a subdirectory at a time. Use both. Diff every changed template by hand, grep for &lt;code&gt;viewChild&lt;/code&gt;/&lt;code&gt;ViewChild&lt;/code&gt;/&lt;code&gt;ngTemplateOutlet&lt;/code&gt; references to any template the migration touched, and don't merge until you've visually verified each converted view — not just that it compiles. Treat the automated migration as a first draft, not a merge-ready PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Flipping to zoneless without auditing implicit change-detection triggers
&lt;/h2&gt;

&lt;p&gt;Angular 21 makes zoneless the default for new apps, and it's increasingly the target for migrated ones too, because it removes an entire class of Zone.js monkey-patching overhead and makes change detection explicit and predictable.&lt;/p&gt;

&lt;p&gt;The useful way to think about the risk is &lt;em&gt;not&lt;/em&gt; "async code stops working" — that's the version of this warning you'll see repeated everywhere, and it's too broad to act on. Zoneless doesn't care whether your code is asynchronous. It cares whether Angular gets &lt;strong&gt;notified&lt;/strong&gt;. Angular's docs list seven conditions that schedule change detection (including edge cases like attaching a dirty view, removing a view, and registering a render hook); these are the ones that matter day to day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a signal that's read in a template is updated&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ChangeDetectorRef.markForCheck()&lt;/code&gt; is called&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ComponentRef.setInput()&lt;/code&gt; is used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a template-bound event listener fires&lt;/strong&gt; — your &lt;code&gt;(click)&lt;/code&gt;, &lt;code&gt;(input)&lt;/code&gt;, &lt;code&gt;(submit)&lt;/code&gt; handlers are all fine&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AsyncPipe&lt;/code&gt; receives a new value
That last point is the whole model in miniature. Angular needs an explicit notification that a view may need updating. A signal write can provide that notification when the signal is read in the template; &lt;code&gt;AsyncPipe&lt;/code&gt; calls &lt;code&gt;markForCheck()&lt;/code&gt; for you when its observable emits. Other Angular APIs, such as &lt;code&gt;ComponentRef.setInput()&lt;/code&gt; and bound template listeners, can also schedule change detection. A hand-rolled subscription that simply assigns a new value to a plain property does none of those things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction is what makes the risk auditable. What breaks is state that changes with &lt;em&gt;no&lt;/em&gt; notification attached — a bare &lt;code&gt;setTimeout&lt;/code&gt; or &lt;code&gt;setInterval&lt;/code&gt; callback, a &lt;code&gt;Promise.then&lt;/code&gt;, a manually-registered &lt;code&gt;addEventListener&lt;/code&gt; outside the template, a WebSocket &lt;code&gt;onmessage&lt;/code&gt;, or a third-party SDK callback, each assigning to a plain property:&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="c1"&gt;// This pattern is everywhere in legacy Angular code&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Zone.js used to notice this; nothing does now&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assignment updates a plain property, and nothing tells Angular a check is needed — the UI silently goes stale. The fix isn't a shim, it's a real architectural change: back that state with a &lt;code&gt;signal()&lt;/code&gt; (or push updates through &lt;code&gt;ChangeDetectorRef.markForCheck()&lt;/code&gt; at minimum) so the framework has an explicit reason to update the view.&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the direction of travel here: an existing app doesn't become zoneless because you upgraded. It keeps its Zone.js behavior until somebody deliberately turns Zone.js off — which means this is a migration you opt into on your own schedule, and therefore one that's easy to defer indefinitely and easy to do carelessly in a quiet sprint by whoever picks up the "remove zone.js" ticket. Before that ticket gets merged, grep your codebase for &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, third-party callbacks, and WebSocket handlers that mutate component state — every one of them is a candidate for silent breakage. Also budget time for your UI kit: Material and PrimeNG components historically had zoneless gaps, so pin to versions explicitly tested against the Angular version you're targeting rather than assuming "latest" works.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Letting the &lt;code&gt;OnPush&lt;/code&gt; migration quietly make your debt permanent
&lt;/h2&gt;

&lt;p&gt;First, a correction to something you'll read on a lot of migration blogs: &lt;strong&gt;zoneless does not require &lt;code&gt;OnPush&lt;/code&gt;.&lt;/strong&gt; Angular's own zoneless guide is explicit — "The &lt;code&gt;OnPush&lt;/code&gt; change detection strategy is not required, but it is a recommended step towards zoneless compatibility." Default-strategy components are still checked in a zoneless app, provided &lt;em&gt;something&lt;/em&gt; notifies Angular that a check is needed. If your app breaks after going zoneless, the cause is a missing notification (mistake #3), not a missing &lt;code&gt;OnPush&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The real trap is on the other side, and it already landed. As of &lt;strong&gt;v22&lt;/strong&gt;, per the release's own breaking-changes note, "Component with undefined &lt;code&gt;changeDetection&lt;/code&gt; property are now &lt;code&gt;OnPush&lt;/code&gt; by default" — the old default is now spelled &lt;code&gt;ChangeDetectionStrategy.Eager&lt;/code&gt; (&lt;a href="https://github.com/angular/angular/discussions/66779" rel="noopener noreferrer"&gt;RFC #66779&lt;/a&gt;; &lt;a href="https://github.com/angular/angular/releases/tag/v22.0.0" rel="noopener noreferrer"&gt;v22.0.0 release notes&lt;/a&gt;). And — exactly as with the standalone migration — the Angular team ships an automatic migration that preserves your behavior. The v22 release describes it as a migration to "add &lt;code&gt;ChangeDetectionStrategy.Eager&lt;/code&gt; where applicable," and it runs as part of &lt;code&gt;ng update&lt;/code&gt; — so components that were relying on the old implicit default come out the other side carrying an explicit &lt;code&gt;Eager&lt;/code&gt; annotation. ("Where applicable" is the release's own wording, so don't assume the annotation count will exactly match your component count; read the diff.)&lt;/p&gt;

&lt;p&gt;That migration is the correct engineering decision by the framework team, and it is also the moment your technical debt becomes invisible. Before it runs, "this component is on the default strategy" is an absence — something you can grep for and gradually fix. After it runs, every one of those components carries an &lt;em&gt;explicit, intentional-looking&lt;/em&gt; &lt;code&gt;Eager&lt;/code&gt; annotation that no future reviewer will question, because it looks like somebody chose it.&lt;/p&gt;

&lt;p&gt;So don't just accept the migration diff and move on. Run it, then treat the resulting &lt;code&gt;Eager&lt;/code&gt; annotations as a &lt;strong&gt;tracked backlog&lt;/strong&gt; rather than settled code — they're a precise, machine-generated inventory of every component that was never designed for &lt;code&gt;OnPush&lt;/code&gt;. Work through them leaf-components-first; the conversion usually surfaces genuine mutation bugs (arrays and objects mutated in place instead of replaced) that were already latent.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Carrying over unmanaged RxJS subscriptions
&lt;/h2&gt;

&lt;p&gt;This one predates zoneless, but the migration is the right moment to deal with it: legacy services and components that call &lt;code&gt;.subscribe()&lt;/code&gt; in &lt;code&gt;ngOnInit&lt;/code&gt; with no &lt;code&gt;takeUntil&lt;/code&gt;, no &lt;code&gt;takeUntilDestroyed()&lt;/code&gt;, and no &lt;code&gt;async&lt;/code&gt; pipe accumulate leaked subscriptions. Repeated destruction and recreation of the component can leave another live subscription behind, holding a reference to the destroyed instance — a memory leak, plus duplicated side effects (double-fired analytics, double-submitted requests) that get harder to trace the longer the session runs. The distinction that matters is whether the source completes: a one-shot &lt;code&gt;HttpClient&lt;/code&gt; call tears its own subscription down, so it's the long-lived streams that bite you — a &lt;code&gt;BehaviorSubject&lt;/code&gt; in a service, router events, &lt;code&gt;fromEvent&lt;/code&gt;, a WebSocket feed.&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="c1"&gt;// Legacy pattern — currentUser$ never completes, so this&lt;/span&gt;
&lt;span class="c1"&gt;// subscription outlives the component on every re-navigation&lt;/span&gt;
&lt;span class="nf"&gt;ngOnInit&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;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser$&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;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Modern pattern&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;destroyRef&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;DestroyRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;ngOnInit&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;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUser$&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;takeUntilDestroyed&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;destroyRef&lt;/span&gt;&lt;span class="p"&gt;))&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;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;user&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;u&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;If you're migrating anyway, this is the moment to convert hand-rolled subscription management to &lt;code&gt;takeUntilDestroyed()&lt;/code&gt;, the &lt;code&gt;async&lt;/code&gt; pipe, or &lt;code&gt;toSignal()&lt;/code&gt; — not just to modernize syntax, but because the old pattern is an active liability once the CD model changes underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Skipping the DI audit when partially migrating to standalone
&lt;/h2&gt;

&lt;p&gt;Scope first, because this one gets overstated: &lt;strong&gt;if your service is &lt;code&gt;providedIn: 'root'&lt;/code&gt;, none of this applies to you.&lt;/strong&gt; It resolves against the root environment injector no matter who injects it, and standalone conversion doesn't change that. Angular has recommended &lt;code&gt;providedIn&lt;/code&gt; as "the best practice for providing services" since v6, so in a reasonably modern codebase most services are already safe.&lt;/p&gt;

&lt;p&gt;The exposure is services registered the old way — listed in an &lt;code&gt;@NgModule.providers&lt;/code&gt; array, typically behind a &lt;code&gt;CoreModule&lt;/code&gt; or a &lt;code&gt;SomeModule.forRoot()&lt;/code&gt;. That pattern already had a well-known duplication hazard in the NgModule world: provide a service in a module that gets imported by both an eagerly-loaded and a lazy-loaded module, and the lazy module's injector instantiates its own copy. &lt;code&gt;forRoot()&lt;/code&gt; exists specifically as a convention to avoid it, and the classic &lt;code&gt;CoreModule&lt;/code&gt; guard — inject the module into itself with &lt;code&gt;@Optional() @SkipSelf()&lt;/code&gt; and throw if it's already there — exists to catch it when the convention is broken.&lt;/p&gt;

&lt;p&gt;Standalone conversion adds a &lt;strong&gt;new way to trip the same wire&lt;/strong&gt;, because it introduces new injector boundaries. If a provider-carrying NgModule is moved into a narrower injector scope during migration — most commonly by landing it in a standalone component's own &lt;code&gt;imports&lt;/code&gt; array to satisfy that component's dependencies — a service that was previously resolved as one shared instance may now be instantiated more than once. The exact behavior depends on how the module and its providers are wired, but the risk is real: changing the injector hierarchy changes the lifetime and scope of services.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical, and it isn't a bug queued for a fix. The Angular team reviewed a report of exactly this — services from an NgModule's &lt;code&gt;providers&lt;/code&gt; duplicating across standalone components that imported it — and closed it as working as designed (&lt;a href="https://github.com/angular/angular/issues/53120" rel="noopener noreferrer"&gt;#53120&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The failure is nasty because it doesn't look like a DI problem. Two copies of &lt;code&gt;CartService&lt;/code&gt; means the header reads one instance and checkout reads another; the symptom is "the cart shows 2 items up top and 0 at checkout," which gets triaged as a state-sync bug and debugged as one, sometimes for days, because nobody suspects the injector topology moved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is to hoist, not to sprinkle.&lt;/strong&gt; Provider-carrying modules belong at application level, not in component &lt;code&gt;imports&lt;/code&gt;:&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="c1"&gt;// Risky during migration: pulling a provider-carrying NgModule&lt;/span&gt;
&lt;span class="c1"&gt;// into a component introduces a narrower provider scope.&lt;/span&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;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CoreModule&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="c1"&gt;// Migration approach: hoist NgModule providers to the&lt;/span&gt;
&lt;span class="c1"&gt;// application/environment injector instead.&lt;/span&gt;
&lt;span class="nf"&gt;bootstrapApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppComponent&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;importProvidersFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CoreModule&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;importProvidersFrom()&lt;/code&gt; is the bridge API for exactly this, and its type signature makes the intended scope explicit. It returns &lt;code&gt;EnvironmentProviders&lt;/code&gt;, and Angular's docs state that providers extracted this way "are only usable in an application injector or another environment injector (such as a route injector). They should not be used in component providers."&lt;/p&gt;

&lt;p&gt;It's a fine migration step, but treat it as a stepping stone rather than a destination — it only carries providers, so anything else the module was doing (initialization logic, guards) needs somewhere to live. The end state most teams want is a plain &lt;code&gt;provideCore()&lt;/code&gt; function returning a &lt;code&gt;Provider[]&lt;/code&gt;, with module-constructor side effects moved to an initializer. Before you convert a single component, grep for &lt;code&gt;@NgModule.providers&lt;/code&gt; arrays and &lt;code&gt;forRoot(&lt;/code&gt; — that list &lt;em&gt;is&lt;/em&gt; your risk register.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. No regression safety net before refactoring
&lt;/h2&gt;

&lt;p&gt;Teams that skip continuous testing during migration aren't cutting a corner, they're removing the only thing that would tell them the migration is wrong. This shows up constantly on real projects: &lt;code&gt;TestBed&lt;/code&gt; configurations built for NgModule-declared components don't automatically work for standalone ones (&lt;code&gt;imports&lt;/code&gt; vs &lt;code&gt;declarations&lt;/code&gt; semantics differ), so the test suite either silently stops covering what it used to, or fails to compile and gets &lt;code&gt;.skip()&lt;/code&gt;'d "temporarily" — which becomes permanent.&lt;/p&gt;

&lt;p&gt;Before touching template syntax or component architecture, get characterization tests (even coarse E2E ones with Cypress/Playwright) around the critical user flows. They don't need to be elegant; they need to fail loudly when the migration breaks behavior that unit tests, written against the old structure, won't catch because they get rewritten alongside the code they're testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Letting CI/CD quietly block the upgrade
&lt;/h2&gt;

&lt;p&gt;Angular's version requirements move fast, and the minimum-patch precision catches people out. Per Angular's own &lt;a href="https://angular.dev/reference/versions" rel="noopener noreferrer"&gt;version compatibility reference&lt;/a&gt;, v20 and v21 both require Node &lt;code&gt;^20.19.0 || ^22.12.0 || ^24.0.0&lt;/code&gt; — note that's 20.&lt;strong&gt;19&lt;/strong&gt;, so a runner pinned to Node 20.18 fails even though it's "on Node 20." v22 raises the floor again to &lt;code&gt;^22.22.3 || ^24.15.0 || ^26.0.0&lt;/code&gt;, dropping Node 20 entirely. Each major typically bumps the minimum TypeScript version too. The mistake is discovering this in CI &lt;em&gt;after&lt;/em&gt; the code changes are already merged to a branch, because the pipeline's Node image was pinned two years ago and nobody owns updating it. This isn't a framework problem, it's an infrastructure-ownership gap — but it blocks the migration just as effectively as a code bug, and it's invisible until someone actually tries to run &lt;code&gt;ng update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check your CI runner's Node/TypeScript versions against the target Angular version's requirements &lt;em&gt;before&lt;/em&gt; scheduling the migration work, not after the PR is open.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Rewriting state management ad hoc, component by component
&lt;/h2&gt;

&lt;p&gt;Legacy Angular apps accumulate state handling organically: some data lives in &lt;code&gt;BehaviorSubject&lt;/code&gt;s in services, some in &lt;code&gt;@Input()&lt;/code&gt;/&lt;code&gt;@Output()&lt;/code&gt; chains, some in route resolvers, some in whatever global singleton someone added under deadline pressure. The mistake during migration is "fixing" this piecemeal — converting whichever service you happen to be touching to signals, NgRx SignalStore, or a plain signal-based store, with no shared decision about which pattern owns which category of state.&lt;/p&gt;

&lt;p&gt;The result, eighteen months later, is a codebase with three competing state paradigms instead of one, which is strictly worse than the single legacy pattern it replaced because now every new developer has to learn which parts of the app use which model. Decide the target state architecture (signals for local/component state, a signal store or NgRx for shared/cross-cutting state, resolvers or route data for navigation-scoped state) &lt;em&gt;before&lt;/em&gt; the migration starts, and treat inconsistency with that decision as a review blocker, not a style preference.&lt;/p&gt;




&lt;h3&gt;
  
  
  The pattern underneath all nine
&lt;/h3&gt;

&lt;p&gt;Almost every mistake here comes from the same root cause: treating the migration as a mechanical version bump instead of an architectural decision with a safety net. The tooling (&lt;code&gt;ng update&lt;/code&gt;, the standalone schematic, the control-flow migration) is good, but it's good at syntax transformation, not at verifying behavior or catching the assumptions your codebase quietly baked in around Zone.js, &lt;code&gt;$scope&lt;/code&gt;, or singleton services. Budget the audit and testing time as part of the migration, not as cleanup afterward — it's cheaper every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick pre-migration checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirm CI's Node/TypeScript versions meet the &lt;em&gt;target&lt;/em&gt; Angular version's exact minimums (patch versions matter)&lt;/li&gt;
&lt;li&gt;Grep for &lt;code&gt;setTimeout&lt;/code&gt;/&lt;code&gt;setInterval&lt;/code&gt;/raw callbacks mutating component state before turning Zone.js off&lt;/li&gt;
&lt;li&gt;Inventory services in NgModule &lt;code&gt;providers&lt;/code&gt; arrays before any standalone conversion — that's where duplicate-instance risk lives&lt;/li&gt;
&lt;li&gt;After the v22 &lt;code&gt;Eager&lt;/code&gt; migration runs, track the generated annotations as a backlog instead of treating them as settled code&lt;/li&gt;
&lt;li&gt;Grep for &lt;code&gt;viewChild&lt;/code&gt;/&lt;code&gt;ViewChild&lt;/code&gt;/&lt;code&gt;ngTemplateOutlet&lt;/code&gt; references to any &lt;code&gt;ng-template&lt;/code&gt; the control-flow migration touched&lt;/li&gt;
&lt;li&gt;Get coarse E2E coverage on critical flows before refactoring &lt;code&gt;TestBed&lt;/code&gt; configs&lt;/li&gt;
&lt;li&gt;Pick one target state-management pattern and write it down before the first PR&lt;/li&gt;
&lt;li&gt;Upgrade one major version at a time — it's enforced, not advisory&lt;/li&gt;
&lt;li&gt;If you're on an ngUpgrade hybrid, sequence it: treat zoneless as a step after AngularJS is gone, not alongside it&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Micro Frontends in Angular: Single-SPA vs Module Federation (When Each One Actually Makes Sense)</title>
      <dc:creator>Amanulla Khan</dc:creator>
      <pubDate>Sun, 12 Jul 2026 19:21:19 +0000</pubDate>
      <link>https://dev.to/amankhan55/micro-frontends-in-angular-single-spa-vs-module-federation-when-each-one-actually-makes-sense-2ofk</link>
      <guid>https://dev.to/amankhan55/micro-frontends-in-angular-single-spa-vs-module-federation-when-each-one-actually-makes-sense-2ofk</guid>
      <description>&lt;p&gt;Micro frontends get pitched as the natural next step after microservices — split the UI the way you split the backend, ship teams independently, deploy without coordination. That pitch is half true. The other half is the part nobody puts in the conference talk: shared state headaches, duplicated bundles, version drift across teams, and a debugging story that gets meaningfully harder.&lt;/p&gt;

&lt;p&gt;This article walks through the two dominant patterns for building Angular micro frontends — &lt;strong&gt;Single-SPA&lt;/strong&gt; and &lt;strong&gt;Module Federation&lt;/strong&gt; — with enough architectural detail to make a real decision, not just a demo that works on your laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the how: should you even do this?
&lt;/h2&gt;

&lt;p&gt;Micro frontends solve an &lt;em&gt;organizational&lt;/em&gt; problem, not a technical one. The technical problem (one big Angular app is slow to build and risky to deploy) has cheaper fixes first: Nx monorepo with incremental builds, lazy-loaded standalone routes, esbuild-based builders. If a single team owns the app, none of those cheaper fixes are exhausted yet, and reaching for micro frontends first is usually premature.&lt;/p&gt;

&lt;p&gt;Micro frontends earn their complexity when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple autonomous teams ship to the same product on independent release cadences&lt;/li&gt;
&lt;li&gt;Different parts of the app genuinely need different tech stacks or major-version Angular/React/Vue coexistence&lt;/li&gt;
&lt;li&gt;A monolith's build/deploy time has become an organizational bottleneck, not just an annoyance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of those are true, skip this article's implementation section and go fix your build pipeline instead. Assume from here that the organizational case is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different philosophies, not two flavors of the same thing
&lt;/h2&gt;

&lt;p&gt;It's tempting to treat Single-SPA and Module Federation as interchangeable "micro frontend tools." They're not — they solve different layers of the problem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Single-SPA&lt;/th&gt;
&lt;th&gt;Webpack Module Federation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it actually is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A top-level router/orchestrator that mounts and unmounts independently-built apps&lt;/td&gt;
&lt;td&gt;A build-time/runtime mechanism for one bundle to consume code from another bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unit of composition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whole applications (each with its own bootstrap lifecycle)&lt;/td&gt;
&lt;td&gt;Individual modules/components, at any granularity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framework agnosticism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong — mixes Angular, React, Vue, vanilla JS as siblings&lt;/td&gt;
&lt;td&gt;Works across frameworks too, but composition is finer-grained and framework boundaries get blurrier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing ownership&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single-SPA owns top-level routing; each app owns its internal routing&lt;/td&gt;
&lt;td&gt;No opinion on routing — you wire it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical use case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"The dashboard is one app, billing is another, teams ship independently"&lt;/td&gt;
&lt;td&gt;"This app needs a button-level component or a specific module from another team's build, sometimes even inside the same route"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each app deployed independently, registered at a known URL/import map&lt;/td&gt;
&lt;td&gt;Each "remote" deployed independently, exposed via a federation manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity in Angular 20 workflow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mature, but Angular Elements wrapping and zone coordination are manual work&lt;/td&gt;
&lt;td&gt;Native Federation (via &lt;code&gt;@angular-architects/native-federation&lt;/code&gt;) replaced Webpack-specific federation as the standard approach for esbuild-based Angular builds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, most real Angular micro frontend architectures at enterprise scale use &lt;strong&gt;both&lt;/strong&gt;: Single-SPA (or a simpler custom shell) for top-level app orchestration, and Module Federation for sharing specific components or libraries across those apps without duplicating them in every bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

&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%2F3556g2j3expftped59bh.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%2F3556g2j3expftped59bh.png" alt="Microfrontend architecture with Single-SPA and Module Federation." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The shell doesn't know or care what's inside Dashboard or Billing. Dashboard and Billing independently pull a shared component (say, a notification bell or an auth-status widget) via Module Federation instead of each bundling their own copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the shell with Single-SPA
&lt;/h2&gt;

&lt;p&gt;A minimal root config for an Angular-based shell looks like this:&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="c1"&gt;// root-config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;single-spa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;registerApplication&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@company/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;app&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="nx"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@company/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;activeWhen&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;/dashboard&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="nf"&gt;registerApplication&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@company/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;app&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="nx"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@company/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;activeWhen&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;/billing&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="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;urlRerouteOnly&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each micro frontend is bootstrapped as a Single-SPA "lifecycle" application — it exports &lt;code&gt;bootstrap&lt;/code&gt;, &lt;code&gt;mount&lt;/code&gt;, and &lt;code&gt;unmount&lt;/code&gt; functions. For Angular, &lt;code&gt;single-spa-angular&lt;/code&gt; wraps this for you:&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="c1"&gt;// dashboard's main.single-spa.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;singleSpaAngular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSingleSpaExtraProviders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;single-spa-angular&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bootstrapApplication&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/platform-browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/app.component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app/app.config&lt;/span&gt;&lt;span class="dl"&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;lifecycles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;singleSpaAngular&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bootstrapFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;singleSpaProps&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;bootstrapApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppComponent&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="nx"&gt;appConfig&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="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getSingleSpaExtraProviders&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;&amp;lt;app-root /&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// let Angular's own router handle internal routes&lt;/span&gt;
  &lt;span class="na"&gt;NgZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unmount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lifecycles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;NgZone&lt;/code&gt; fork. This is the detail that separates a demo from a production setup — more on it below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing code with Module Federation (Native Federation for Angular)
&lt;/h2&gt;

&lt;p&gt;For Angular apps built with the modern esbuild-based builder, &lt;code&gt;@angular-architects/native-federation&lt;/code&gt; is the current standard — it replaced raw Webpack Module Federation config for teams on Angular's default build pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote (exposing a shared library):&lt;/strong&gt;&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="c1"&gt;// federation.config.js (remote: shared-ui)&lt;/span&gt;
&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withNativeFederation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exposes&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;./NotificationBell&lt;/span&gt;&lt;span class="dl"&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;./src/app/notification-bell.component.ts&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;shared&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;shareAll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;singleton&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="na"&gt;strictVersion&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;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Host (consuming it):&lt;/strong&gt;&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="c1"&gt;// federation.config.js (host: dashboard)&lt;/span&gt;
&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withNativeFederation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;shared&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;shareAll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;singleton&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="na"&gt;strictVersion&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;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lazy-loading the remote component in a route&lt;/span&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="s1"&gt;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;loadComponent&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;loadRemoteModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;exposedModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./NotificationBell&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NotificationBellComponent&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;Because &lt;code&gt;NotificationBellComponent&lt;/code&gt; is a standalone component, there's no NgModule ceremony on either side — this is one of the concrete reasons Angular 20+'s standalone-first posture matters for micro frontends specifically: fewer surfaces where two apps' module graphs can conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change detection implications — the part most tutorials skip
&lt;/h2&gt;

&lt;p&gt;This is where micro frontends stop being a routing problem and become an Angular internals problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zone.js collisions.&lt;/strong&gt; By default, every Angular app monkey-patches the same global async APIs (&lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;addEventListener&lt;/code&gt;, &lt;code&gt;Promise&lt;/code&gt;, XHR) through Zone.js. Load two independently-bootstrapped Angular apps on the same page without isolating their zones, and you get one of two failure modes: change detection storms (one app's async event triggers CD in both apps) or, worse, silent CD skips where updates in app B never render because zone context got lost crossing into app A's execution. The &lt;code&gt;Zone.current.fork()&lt;/code&gt; call above isn't boilerplate — it's what keeps each MFE's change detection cycle scoped to itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoneless changes the calculus.&lt;/strong&gt; Angular 18+'s zoneless change detection (stabilized further in 20) sidesteps the zone-collision problem entirely for apps that adopt it, because there's no shared monkey-patched global state to collide over — each app's signals and &lt;code&gt;ChangeDetectorRef.markForCheck()&lt;/code&gt; calls are self-contained. If you're architecting a &lt;em&gt;new&lt;/em&gt; micro frontend system today, defaulting every MFE to zoneless is one of the highest-leverage decisions you can make, precisely because it removes a whole category of cross-app bugs before they exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Singleton services aren't singletons anymore.&lt;/strong&gt; &lt;code&gt;providedIn: 'root'&lt;/code&gt; means root of &lt;em&gt;that app's&lt;/em&gt; injector tree. Two federated apps each get their own instance of what looks like a shared service — including shared state services, auth token stores, and RxJS &lt;code&gt;BehaviorSubject&lt;/code&gt;-based stores. If Dashboard and Billing both import an "auth state service" via Module Federation but each ends up with its own instance, they'll silently drift out of sync. The fix is either: promote true cross-cutting state to the shell and pass it down via custom events / a shared event bus, or explicitly share the service instance through Module Federation's &lt;code&gt;shared&lt;/code&gt; config with &lt;code&gt;singleton: true&lt;/code&gt; and verify at runtime that both apps resolved the same instance (log the object identity in dev, don't just assume the config worked).&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate bundle weight is the default failure mode&lt;/strong&gt;, not an edge case. Without careful &lt;code&gt;shared&lt;/code&gt; config, every MFE ships its own copy of Angular core, RxJS, and any common UI library — multiplying total payload by the number of MFEs on the page. &lt;code&gt;shareAll({ singleton: true })&lt;/code&gt; mitigates this but only for packages that are actually declared shared and version-compatible; a silent version mismatch causes federation to fall back to bundling a second copy, and that fallback fails quietly unless you're watching bundle analyzer output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waterfall loading across remotes.&lt;/strong&gt; A host loading a remote that itself loads another remote creates a request waterfall invisible in local dev (everything's on localhost, latency is near zero) but very visible in production over real network conditions. Model this with WebPageTest or Chrome DevTools network throttling before shipping, not after a complaint ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift is a runtime bug, not a build error.&lt;/strong&gt; Module Federation resolves shared dependency versions at runtime. Two teams shipping independently can produce a combination that was never tested together, and it'll only surface as a production error, not a build failure. This is the single biggest argument for contract testing between MFEs, not just unit tests within each one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common anti-patterns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Micro frontends that share a database-level of coupling in the UI layer&lt;/strong&gt; — if Billing can't render without deep knowledge of Dashboard's internal component state, you've built a distributed monolith with extra deployment steps, not an independent system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shared design system, ad hoc CSS per MFE&lt;/strong&gt; — leads to visual drift and specificity wars when MFEs are composed on the same page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating Module Federation as a dependency injection replacement&lt;/strong&gt; — it's a code-loading mechanism, not an architecture. It won't stop teams from building tightly coupled contracts unless you deliberately design against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Interview questions this topic tends to produce
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How does Angular's Zone.js cause conflicts when multiple Angular apps run on the same page, and how would you isolate them?&lt;/li&gt;
&lt;li&gt;Walk through what happens if two federated remotes declare incompatible versions of a shared singleton service.&lt;/li&gt;
&lt;li&gt;Single-SPA vs Module Federation — when would you use one without the other?&lt;/li&gt;
&lt;li&gt;How does moving to zoneless change detection affect micro frontend architecture decisions?&lt;/li&gt;
&lt;li&gt;What's your strategy for testing integration between independently-deployed MFEs before they reach production?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenge exercise
&lt;/h2&gt;

&lt;p&gt;Take a small Angular app with two standalone feature routes. Split it into a shell (Single-SPA) plus one Module Federation remote exposing a shared component with its own &lt;code&gt;BehaviorSubject&lt;/code&gt;-backed state service. Verify — with actual console logging of instance identity, not assumption — whether the shell and the remote end up with one shared instance of that service or two. Then fix it so they share one, and write down exactly which config line made the difference.&lt;/p&gt;




</description>
      <category>angular</category>
      <category>microfrontends</category>
      <category>webpack</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Angular Signals in 2026: From Fundamentals to Architect-Level Patterns</title>
      <dc:creator>Amanulla Khan</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:38:49 +0000</pubDate>
      <link>https://dev.to/amankhan55/angular-signals-in-2026-from-fundamentals-to-architect-level-patterns-55l5</link>
      <guid>https://dev.to/amankhan55/angular-signals-in-2026-from-fundamentals-to-architect-level-patterns-55l5</guid>
      <description>&lt;p&gt;Angular used to be the framework you apologized for. Too much boilerplate, too much Zone.js magic, too much RxJS ceremony for a checkbox. Somewhere between v16 and v22, that changed. Signals are no longer "a new primitive to learn" — as of &lt;strong&gt;Angular 22 (June 2026)&lt;/strong&gt;, &lt;code&gt;OnPush&lt;/code&gt; is the default change detection strategy, zoneless is the default for new apps, and &lt;strong&gt;Signal Forms and the Resource API are stable&lt;/strong&gt;. Angular is now, by design, a signal-first framework.&lt;/p&gt;

&lt;p&gt;This article covers the full arc: what signals are, how they work internally, how they change your architecture and performance story, how they compare to RxJS, and how to actually migrate a real codebase — plus the interview questions you'll get asked about all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The fundamentals
&lt;/h2&gt;

&lt;p&gt;A signal is a wrapper around a value that knows who's reading it.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&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;count&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="c1"&gt;// writable signal&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doubled&lt;/span&gt; &lt;span class="o"&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;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// derived, read-only, memoized&lt;/span&gt;

&lt;span class="nf"&gt;effect&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`count is now &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;count&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;span class="c1"&gt;// reruns when count changes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;count&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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;=&amp;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;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three primitives, three jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;signal()&lt;/code&gt;&lt;/strong&gt; — holds state, read by calling it as a function, written via &lt;code&gt;.set()&lt;/code&gt; / &lt;code&gt;.update()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;computed()&lt;/code&gt;&lt;/strong&gt; — derives state. Lazy and memoized: it only recalculates when a dependency actually changed &lt;em&gt;and&lt;/em&gt; someone reads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;effect()&lt;/code&gt;&lt;/strong&gt; — runs side effects (logging, syncing to localStorage, imperative DOM work) in response to signal changes. Not for driving template state — if you're setting another signal inside an effect to keep it "in sync," you probably wanted &lt;code&gt;computed()&lt;/code&gt; or &lt;code&gt;linkedSignal()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angular 20 graduated all of these — plus signal-based &lt;code&gt;input()&lt;/code&gt;, &lt;code&gt;model()&lt;/code&gt;, and signal queries (&lt;code&gt;viewChild&lt;/code&gt;, &lt;code&gt;contentChild&lt;/code&gt;) — to stable. If you're on 20+, this is no longer "the new way," it's &lt;em&gt;the&lt;/em&gt; way for new components.&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-user-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;standalone&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="na"&gt;template&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCardComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;userId&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;// signal-based @Input&lt;/span&gt;
  &lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&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="c1"&gt;// two-way bindable signal&lt;/span&gt;
  &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;viewChild&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ElementRef&lt;/span&gt;&lt;span class="o"&gt;&amp;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;header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// signal-based @ViewChild&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. How signals actually work (the internals interviewers ask about)
&lt;/h2&gt;

&lt;p&gt;Signals implement a &lt;strong&gt;push-pull reactive model&lt;/strong&gt;, and this distinction is the single most-tested concept in interviews:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt;: when a writable signal changes, it immediately (synchronously) notifies its direct consumers that they're "dirty." This is cheap — it's just a flag flip, not a recomputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull&lt;/strong&gt;: the actual recomputation only happens lazily, when something &lt;em&gt;reads&lt;/em&gt; the dirty signal — inside a &lt;code&gt;computed()&lt;/code&gt; or during change detection when a template reads it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why signals are &lt;strong&gt;glitch-free&lt;/strong&gt;. In a naive push-based system, if &lt;code&gt;computed&lt;/code&gt; &lt;code&gt;C&lt;/code&gt; depends on both &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt;, and you update &lt;code&gt;A&lt;/code&gt; then &lt;code&gt;B&lt;/code&gt; in the same tick, &lt;code&gt;C&lt;/code&gt; might recompute twice and briefly observe an inconsistent intermediate state. Angular's dependency graph batches this: &lt;code&gt;C&lt;/code&gt; is marked dirty once, and only recomputes once, on next read, with both updates applied.&lt;/p&gt;

&lt;p&gt;Under the hood, each signal maintains a version counter. A &lt;code&gt;computed()&lt;/code&gt; caches its last value and the versions of the signals it read last time. On read, it compares versions; if nothing upstream changed, it returns the cached value without re-executing the function — that's the memoization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency tracking is automatic and dynamic.&lt;/strong&gt; There's no subscribe/unsubscribe. &lt;code&gt;computed()&lt;/code&gt; and &lt;code&gt;effect()&lt;/code&gt; track &lt;em&gt;whatever signals were actually read during their last execution&lt;/em&gt; — so a conditional read (&lt;code&gt;isAdmin() ? adminData() : userData()&lt;/code&gt;) only creates a dependency on the branch that actually executed. Change the condition, and the dependency set changes on the next run. This is structurally similar to Vue's reactivity system and SolidJS, and it's a fair comparison to bring up in an interview.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Change detection implications (this is the part senior candidates get wrong)
&lt;/h2&gt;

&lt;p&gt;Pre-signals Angular used Zone.js to monkey-patch async APIs (&lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;addEventListener&lt;/code&gt;, promises) so it could guess &lt;em&gt;something might have changed&lt;/em&gt; and run change detection on the whole component tree from the root. It works, but it's a blunt instrument — you get checks you don't need, and debugging "why did CD run" is miserable.&lt;/p&gt;

&lt;p&gt;Signals flip this to &lt;strong&gt;explicit, targeted notification&lt;/strong&gt;. A component reading a signal in its template gets automatically marked for a targeted check when that signal changes — no zone patching, no tree-wide guessing.&lt;/p&gt;

&lt;p&gt;As of &lt;strong&gt;Angular 22, &lt;code&gt;OnPush&lt;/code&gt; is the default &lt;code&gt;changeDetection&lt;/code&gt; strategy&lt;/strong&gt; for new components (the old "Default" strategy is renamed &lt;code&gt;Eager&lt;/code&gt; and deprecated). Combined with zoneless (stable default since Angular 21), the practical effect is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No Zone.js in your bundle (smaller payload, faster startup).&lt;/li&gt;
&lt;li&gt;Change detection runs only for components whose signals actually changed, not the whole tree.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;markForCheck()&lt;/code&gt; calls scattered through legacy code become mostly unnecessary — signals do it for you.&lt;/li&gt;
&lt;li&gt;Non-signal mutations (mutating a plain array/object field that CD can't observe) silently stop working correctly under zoneless. This is the #1 zoneless migration bug — reaching into &lt;code&gt;this.items.push(x)&lt;/code&gt; on a plain field instead of &lt;code&gt;this.items.update(arr =&amp;gt; [...arr, x])&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interview question you should be able to answer cold&lt;/strong&gt;: &lt;em&gt;"Why does OnPush + signals outperform Zone.js-based default CD, and what's the one thing that breaks silently when you go zoneless?"&lt;/em&gt; Answer: targeted vs. tree-wide checks, and the answer to the second half is exactly the mutation issue above.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;linkedSignal&lt;/code&gt; and &lt;code&gt;resource()&lt;/code&gt; — the pieces that make signals a full state system
&lt;/h2&gt;

&lt;p&gt;Two APIs turn signals from "a better &lt;code&gt;@Input&lt;/code&gt;" into an actual state-management layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;linkedSignal&lt;/code&gt;&lt;/strong&gt; (stable since Angular 19) is a writable signal that resets itself when a source signal changes — the classic "selected item resets when the list changes" problem, without an &lt;code&gt;effect()&lt;/code&gt; fighting your &lt;code&gt;computed()&lt;/code&gt;:&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;items&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&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;b&lt;/span&gt;&lt;span class="dl"&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;c&lt;/span&gt;&lt;span class="dl"&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;selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// resets to items()[0] whenever items() changes&lt;/span&gt;
&lt;span class="nx"&gt;selected&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// but user can still override it locally until items() changes again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;resource()&lt;/code&gt; / &lt;code&gt;httpResource()&lt;/code&gt;&lt;/strong&gt; (stable in Angular 22) replace the "subscribe in ngOnInit, manage loading/error flags by hand" pattern with a declarative async primitive:&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;userId&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="nx"&gt;userResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;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="s2"&gt;`/api/users/&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;userId&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;span class="c1"&gt;// userResource.value(), userResource.status(), userResource.error(), userResource.isLoading()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;userId&lt;/code&gt;, and the resource automatically re-fetches — no manual &lt;code&gt;switchMap&lt;/code&gt;, no subscription teardown. As of Angular 21.2, &lt;code&gt;snapshot()&lt;/code&gt; lets you derive a &lt;em&gt;new&lt;/em&gt; resource from an existing one (e.g., a filtered/paginated view) without touching the original fetch logic — genuinely useful for enterprise dashboards with shared base data and multiple derived views.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Signals vs. RxJS — pick the right tool, not a religion
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Signals&lt;/th&gt;
&lt;th&gt;RxJS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Synchronous UI state, derived values, template bindings&lt;/td&gt;
&lt;td&gt;Streams over time: websockets, typeahead debounce, complex event composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mental model&lt;/td&gt;
&lt;td&gt;Pull-based, glitch-free, always has a current value&lt;/td&gt;
&lt;td&gt;Push-based, operators, may never emit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;Minimal — no subscribe/unsubscribe&lt;/td&gt;
&lt;td&gt;Higher — operators, &lt;code&gt;takeUntilDestroyed&lt;/code&gt;, memory-leak discipline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composability&lt;/td&gt;
&lt;td&gt;Growing (&lt;code&gt;linkedSignal&lt;/code&gt;, &lt;code&gt;resource&lt;/code&gt;) but narrower&lt;/td&gt;
&lt;td&gt;Extremely rich (60+ operators)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angular's stance (2026)&lt;/td&gt;
&lt;td&gt;Default for component state&lt;/td&gt;
&lt;td&gt;Still first-class for genuine async streams&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Angular team has been explicit: &lt;strong&gt;RxJS isn't going away.&lt;/strong&gt; &lt;code&gt;toSignal()&lt;/code&gt; / &lt;code&gt;toObservable()&lt;/code&gt; interop exists precisely because you'll keep both in the same codebase — RxJS for a websocket-driven live feed, signals for the UI state that renders it. Treat "should I use signals or RxJS" the way you'd treat "should I use a &lt;code&gt;Map&lt;/code&gt; or an array" — depends on the shape of the problem, not on which one is newer.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Migrating a real codebase (NgModules + RxJS → Signals)
&lt;/h2&gt;

&lt;p&gt;Don't do a big-bang rewrite. The Angular team's own guidance, and what holds up in practice on large enterprise apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade to a supported version first.&lt;/strong&gt; Combining a multi-version jump with a signals migration is the most common way these efforts stall out — get current, &lt;em&gt;then&lt;/em&gt; modernize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert leaf components first.&lt;/strong&gt; Presentational components with &lt;code&gt;@Input()&lt;/code&gt;/&lt;code&gt;@Output()&lt;/code&gt; are low-risk: swap to &lt;code&gt;input()&lt;/code&gt;/&lt;code&gt;output()&lt;/code&gt;, verify, move up the tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace &lt;code&gt;BehaviorSubject&lt;/code&gt;-as-state with &lt;code&gt;signal()&lt;/code&gt;.&lt;/strong&gt; If a &lt;code&gt;BehaviorSubject&lt;/code&gt; exists purely to hold "current value UI reads," it's a signal in disguise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep genuine streams as Observables&lt;/strong&gt;, bridge with &lt;code&gt;toSignal()&lt;/code&gt; at the template boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on &lt;code&gt;OnPush&lt;/code&gt; explicitly before you touch signals&lt;/strong&gt;, so you isolate "did this break because of CD strategy" from "did this break because of signals."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zoneless last.&lt;/strong&gt; It's the biggest behavioral change (silent mutation bugs) — do it once signals are already load-bearing in the app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  7. Enterprise example: a filtered order dashboard
&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-order-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;standalone&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="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&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="s2"&gt;`
    &amp;lt;input [ngModel]="statusFilter()" (ngModelChange)="statusFilter.set($event)" /&amp;gt;
    @if (ordersResource.isLoading()) { &amp;lt;app-spinner /&amp;gt; }
    @for (order of filteredOrders(); track order.id) {
      &amp;lt;app-order-row [order]="order" /&amp;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;OrderDashboardComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;customerId&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="nx"&gt;statusFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipped&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;ordersResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&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="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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`/api/customers/&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;customerId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;/orders`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;filteredOrders&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&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="nx"&gt;ordersResource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;statusFilter&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;effect&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="c1"&gt;// side effect only — analytics, not state&lt;/span&gt;
      &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard_filtered&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;status&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;statusFilter&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;Notice what's &lt;em&gt;absent&lt;/em&gt;: no &lt;code&gt;ngOnInit&lt;/code&gt; subscription, no &lt;code&gt;loading&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt; boolean fields, no manual unsubscribe, no &lt;code&gt;markForCheck()&lt;/code&gt;. Changing &lt;code&gt;customerId()&lt;/code&gt; re-fetches automatically; changing &lt;code&gt;statusFilter()&lt;/code&gt; re-filters automatically; both are independently cached and only recompute what actually needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Common anti-patterns
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effects that write signals to "sync" state.&lt;/strong&gt; If &lt;code&gt;effect()&lt;/code&gt; exists only to call &lt;code&gt;.set()&lt;/code&gt; on another signal, use &lt;code&gt;computed()&lt;/code&gt; (or &lt;code&gt;linkedSignal()&lt;/code&gt; if it needs local override capability) instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading a signal outside a reactive context and expecting reactivity.&lt;/strong&gt; Calling &lt;code&gt;count()&lt;/code&gt; in a regular method gives you a snapshot, not a subscription — reactivity only happens inside &lt;code&gt;computed&lt;/code&gt;, &lt;code&gt;effect&lt;/code&gt;, or a template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutating instead of replacing&lt;/strong&gt; (&lt;code&gt;array.push()&lt;/code&gt; vs &lt;code&gt;.update(a =&amp;gt; [...a, x])&lt;/code&gt;) — silently breaks under zoneless/OnPush since the reference never changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overusing &lt;code&gt;effect()&lt;/code&gt; for cross-component communication&lt;/strong&gt; instead of a shared injectable signal/service — reintroduces the debugging pain signals were meant to remove.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Interview questions to be ready for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Explain the push-pull model and why it makes signals glitch-free.&lt;/li&gt;
&lt;li&gt;What's the practical difference between &lt;code&gt;computed()&lt;/code&gt; and &lt;code&gt;effect()&lt;/code&gt;, and when would using one instead of the other cause a bug?&lt;/li&gt;
&lt;li&gt;Why does OnPush change detection pair naturally with signals, and what breaks when you combine plain-object mutation with zoneless CD?&lt;/li&gt;
&lt;li&gt;How does dynamic dependency tracking work, and what happens to a computed's dependency list when a conditional branch changes?&lt;/li&gt;
&lt;li&gt;When would you &lt;em&gt;still&lt;/em&gt; reach for RxJS over signals in a signals-first Angular 22 app?&lt;/li&gt;
&lt;li&gt;What does &lt;code&gt;linkedSignal&lt;/code&gt; solve that a plain &lt;code&gt;computed()&lt;/code&gt; can't?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Challenge exercise
&lt;/h2&gt;

&lt;p&gt;Build a typeahead search component using &lt;code&gt;httpResource()&lt;/code&gt; and &lt;code&gt;linkedSignal()&lt;/code&gt;: a &lt;code&gt;signal&amp;lt;string&amp;gt;()&lt;/code&gt; for the raw query, a &lt;code&gt;computed()&lt;/code&gt; or &lt;code&gt;linkedSignal()&lt;/code&gt; for a debounced/trimmed version (hint: you'll actually want RxJS's &lt;code&gt;debounceTime&lt;/code&gt; here via &lt;code&gt;toSignal(toObservable(query).pipe(debounceTime(300)))&lt;/code&gt; — a good exercise in knowing exactly where the signals/RxJS boundary should sit), and a &lt;code&gt;resource()&lt;/code&gt; that re-fetches on the debounced value. Add a &lt;code&gt;linkedSignal&lt;/code&gt; for "selected result" that resets whenever the result list changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Signals aren't a syntax swap for &lt;code&gt;@Input()&lt;/code&gt; — they're Angular's answer to "how do we know exactly what changed, without guessing." That answer now shapes change detection defaults, forms, async data fetching, and DI. If you're prepping for a senior/architect role, the framework-trivia questions ("what's the syntax for &lt;code&gt;computed&lt;/code&gt;") are table stakes; the questions that actually separate candidates are about the &lt;em&gt;reactive graph&lt;/em&gt; — push vs. pull, dependency tracking, and where the signals/RxJS boundary should sit in a real system.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
