<?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: Hulpoi George</title>
    <description>The latest articles on DEV Community by Hulpoi George (@georgehulpoi).</description>
    <link>https://dev.to/georgehulpoi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3681769%2Ff9f5861b-aa69-4907-9069-83c0c966bdf0.jpeg</url>
      <title>DEV Community: Hulpoi George</title>
      <link>https://dev.to/georgehulpoi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/georgehulpoi"/>
    <language>en</language>
    <item>
      <title>Angular Zoneless: Migrating off Zone.js without breaking your UI</title>
      <dc:creator>Hulpoi George</dc:creator>
      <pubDate>Sat, 27 Dec 2025 20:15:17 +0000</pubDate>
      <link>https://dev.to/georgehulpoi/angular-zoneless-migrating-off-zonejs-without-breaking-your-ui-3cek</link>
      <guid>https://dev.to/georgehulpoi/angular-zoneless-migrating-off-zonejs-without-breaking-your-ui-3cek</guid>
      <description>&lt;p&gt;You know that feeling when you change a value in a callback… and Angular “magically” updates the UI?&lt;/p&gt;

&lt;p&gt;Until it doesn’t.&lt;/p&gt;

&lt;p&gt;That “magic” has historically been &lt;strong&gt;Zone.js&lt;/strong&gt;. And as Angular moves toward &lt;strong&gt;Zoneless&lt;/strong&gt; change detection, a lot of apps are discovering just how much they were relying on &lt;em&gt;incidental&lt;/em&gt; change detection ticks.&lt;/p&gt;

&lt;p&gt;This post is a practical, dev-to-dev walkthrough of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what Zone.js did for Angular&lt;/li&gt;
&lt;li&gt;what replaces it in an &lt;strong&gt;Angular Zoneless&lt;/strong&gt; app&lt;/li&gt;
&lt;li&gt;what breaks during migration (and why)&lt;/li&gt;
&lt;li&gt;patterns that “just work” (signals / AsyncPipe)&lt;/li&gt;
&lt;li&gt;how to handle the biggest hotspot: third‑party callbacks&lt;/li&gt;
&lt;li&gt;testing gotchas&lt;/li&gt;
&lt;li&gt;a step-by-step migration plan you can actually follow&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Problem: Zone.js made change detection feel effortless
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Zone.js did for Angular (baseline to replace)
&lt;/h3&gt;

&lt;p&gt;Zone.js monkey-patches async browser APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Promise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addEventListener&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;XHR / fetch-like APIs&lt;/li&gt;
&lt;li&gt;…and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angular’s &lt;code&gt;NgZone&lt;/code&gt; listens to Zone.js’ “the microtask queue is empty” signal and responds by running a global change detection pass:&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;// Pseudocode of the zone-driven model&lt;/span&gt;
&lt;span class="nx"&gt;ngZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMicrotaskEmpty&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;appRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So any async work—&lt;em&gt;anywhere&lt;/em&gt;—could eventually trigger an Angular refresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agitation: That “magic tick” was also a tax (and a source of bugs)
&lt;/h2&gt;

&lt;p&gt;Zone-based change detection has a couple of real-world pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard-to-predict Change Detection triggers&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A third-party SDK schedules a timer → you get a full app check.&lt;/li&gt;
&lt;li&gt;Some unrelated listener fires → another tick.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Performance overhead&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Patching async APIs isn’t free.&lt;/li&gt;
&lt;li&gt;Extra change detection passes add up, especially in large apps.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Stability is tricky&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Tests/SSR often depend on “is the zone stable yet?” semantics.&lt;/li&gt;
&lt;li&gt;It can be hard to reason about what “stable” even means when &lt;em&gt;everything&lt;/em&gt; is tracked.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In other words: Zone.js often made things &lt;em&gt;work&lt;/em&gt;, but not always in ways you’d intentionally design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution: Angular Zoneless change detection is explicit and framework-driven
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core idea
&lt;/h3&gt;

&lt;p&gt;In an &lt;strong&gt;Zoneless&lt;/strong&gt; app, Angular moves away from global async patching and toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;known framework entry points&lt;/strong&gt; (template events, router, http, forms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;explicit reactive state updates&lt;/strong&gt; (signals, AsyncPipe emissions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of “tick after any async completes”, Angular uses a &lt;strong&gt;change detection scheduler&lt;/strong&gt; to queue/coalesce updates (often to a microtask or animation frame).&lt;/p&gt;

&lt;h3&gt;
  
  
  What triggers UI refresh in a Zoneless app?
&lt;/h3&gt;

&lt;p&gt;Primary trigger sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Template event handlers&lt;/strong&gt; like &lt;code&gt;(click)&lt;/code&gt; → marks the relevant view tree dirty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal writes&lt;/strong&gt; (&lt;code&gt;signal.set()&lt;/code&gt; / &lt;code&gt;.update()&lt;/code&gt;) → refresh dependent views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AsyncPipe&lt;/code&gt; emissions&lt;/strong&gt; → marks view dirty on emission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Angular-managed subsystems&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Router navigations&lt;/li&gt;
&lt;li&gt;HttpClient observables&lt;/li&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual triggers&lt;/strong&gt; for “unknown async”

&lt;ul&gt;
&lt;li&gt;third-party callbacks&lt;/li&gt;
&lt;li&gt;raw timers&lt;/li&gt;
&lt;li&gt;custom DOM listeners&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the heart of &lt;strong&gt;Angular Zoneless&lt;/strong&gt;: &lt;em&gt;Angular updates when Angular knows something changed.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Enabling Zoneless mode (bootstrap-level)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The API name has changed across versions while the feature matured. In Angular versions where it is still flagged as experimental/dev-preview, it is typically exposed as &lt;code&gt;provideExperimentalZonelessChangeDetection()&lt;/code&gt; from &lt;code&gt;@angular/core&lt;/code&gt;. Use the exact function name provided by your installed Angular version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Standalone bootstrap example:&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;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;provideExperimentalZonelessChangeDetection&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="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="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;provideExperimentalZonelessChangeDetection&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;Removing Zone.js from the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove &lt;code&gt;zone.js&lt;/code&gt; import from &lt;code&gt;polyfills.ts&lt;/code&gt; (or the equivalent &lt;code&gt;angular.json&lt;/code&gt; “polyfills” entry).&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;zone.js/testing&lt;/code&gt; from test setup if you intend to run tests without Zone (more on that later).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Patterns that “just work” in Zoneless apps (and why)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Works: signals (preferred primitive)
&lt;/h3&gt;

&lt;p&gt;Signals are first-class in zoneless because writes are explicit.&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;Component&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="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="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-counter&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;`
    &amp;lt;button (click)="inc()"&amp;gt;+&amp;lt;/button&amp;gt;
    &amp;lt;p&amp;gt;Count: {{ count() }}&amp;lt;/p&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;CounterComponent&lt;/span&gt; &lt;span class="p"&gt;{&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="nf"&gt;inc&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;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;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&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;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;Why this works: Angular knows exactly when &lt;code&gt;count&lt;/code&gt; changes and can schedule refresh precisely.&lt;/p&gt;




&lt;h3&gt;
  
  
  Works: &lt;code&gt;AsyncPipe&lt;/code&gt; (RxJS is still totally fine)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AsyncPipe&lt;/code&gt; marks the view dirty on emission (zoneless-friendly).&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;Component&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&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;rxjs&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="nd"&gt;Component&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;`{{ value$ | async }}`&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;TickerComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`t=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Works: &lt;code&gt;toSignal()&lt;/code&gt; bridge (RxJS → signals)
&lt;/h3&gt;

&lt;p&gt;This avoids manual subscriptions and keeps updates explicit.&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;Component&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;inject&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toSignal&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/rxjs-interop&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;HttpClient&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/common/http&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="nd"&gt;Component&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;ng-container *ngIf="user(); else loading"&amp;gt;
      {{ user()!.name }}
    &amp;lt;/ng-container&amp;gt;
    &amp;lt;ng-template #loading&amp;gt;Loading...&amp;lt;/ng-template&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;UserComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;http&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;HttpClient&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="nf"&gt;toSignal&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;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/me&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;initialValue&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common breakage: “unknown async” stops updating your UI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Symptom
&lt;/h3&gt;

&lt;p&gt;State updates happen, but UI doesn’t refresh because Angular wasn’t notified.&lt;/p&gt;

&lt;p&gt;This is the migration moment where people say: “Wait… Angular is broken.”&lt;/p&gt;

&lt;p&gt;It isn’t. You just removed the global “tick whenever anything async finishes” mechanism.&lt;/p&gt;




&lt;h3&gt;
  
  
  Case 1: Raw timers mutating plain fields
&lt;/h3&gt;

&lt;p&gt;Bad (in zoneless):&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="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="nf"&gt;setInterval&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;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// UI may not update&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Fix A: use a signal
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&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="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="nf"&gt;setInterval&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;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;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&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;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// updates UI&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Fix B: manual notification (&lt;code&gt;markForCheck()&lt;/code&gt;)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;cdr&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;ChangeDetectorRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="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="nf"&gt;setInterval&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;count&lt;/span&gt;&lt;span class="o"&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;cdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markForCheck&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// schedules check for this component subtree&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick warning: &lt;code&gt;detectChanges()&lt;/code&gt; is synchronous and can be expensive or unsafe in some lifecycle moments. In zoneless apps, &lt;strong&gt;&lt;code&gt;markForCheck()&lt;/code&gt; is usually the right “schedule it” primitive&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Third-party libraries and DOM callbacks (the biggest migration hotspot)
&lt;/h2&gt;

&lt;p&gt;This is where most real apps feel the pain first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem class
&lt;/h3&gt;

&lt;p&gt;Libraries that call you back from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;native DOM listeners they register themselves&lt;/li&gt;
&lt;li&gt;WebSocket callbacks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postMessage&lt;/code&gt; handlers&lt;/li&gt;
&lt;li&gt;custom schedulers / &lt;code&gt;requestAnimationFrame&lt;/code&gt; loops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In zone-based Angular, those callbacks often triggered Change Detection “by accident” due to patching.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Angular Zoneless&lt;/strong&gt;, Angular won’t notice unless you tell it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategies (pick one) for third-party callbacks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Write to signals inside the callback (best)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&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;data&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;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// signal write -&amp;gt; schedules refresh&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;This is the cleanest migration path because it turns “random callback” into “explicit state update”.&lt;/p&gt;




&lt;h3&gt;
  
  
  Option 2: Manual &lt;code&gt;markForCheck()&lt;/code&gt; for plain fields
&lt;/h3&gt;

&lt;p&gt;Useful when refactoring to signals isn’t feasible yet.&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;private&lt;/span&gt; &lt;span class="nx"&gt;cdr&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;ChangeDetectorRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nl"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&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;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;cdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markForCheck&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;h3&gt;
  
  
  Option 3: Wrap callbacks into Angular-managed reactive sources
&lt;/h3&gt;

&lt;p&gt;Convert callbacks to RxJS and bind via &lt;code&gt;AsyncPipe&lt;/code&gt; / &lt;code&gt;toSignal()&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;messages$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MessageEvent&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;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One pitfall to keep in mind: teardown. Make sure you unsubscribe/complete properly. Zone removal doesn’t create leaks—but it can remove “incidental” stability behavior you may have been leaning on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Change detection strategy guidance for Zoneless apps
&lt;/h2&gt;

&lt;p&gt;A practical mental shift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OnPush becomes the default model&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Change Detection should run because:

&lt;ul&gt;
&lt;li&gt;an input changed&lt;/li&gt;
&lt;li&gt;an event happened&lt;/li&gt;
&lt;li&gt;a signal changed&lt;/li&gt;
&lt;li&gt;an observable emitted&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Recommendations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;ChangeDetectionStrategy.OnPush&lt;/code&gt; broadly&lt;/li&gt;
&lt;li&gt;Prefer signals for local UI state&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;AsyncPipe&lt;/code&gt; / &lt;code&gt;toSignal()&lt;/code&gt; for streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pitfall to watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in-place mutations like &lt;code&gt;this.model.items.push(...)&lt;/code&gt; + OnPush can hide updates unless:

&lt;ul&gt;
&lt;li&gt;the mutation is behind a signal write, or&lt;/li&gt;
&lt;li&gt;you call &lt;code&gt;markForCheck()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Router / HttpClient / Forms considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HttpClient
&lt;/h3&gt;

&lt;p&gt;Usually fine because it’s RxJS-based.&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;// OK with AsyncPipe&lt;/span&gt;
&lt;span class="nx"&gt;data$&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;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Data&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;/api/data&lt;/span&gt;&lt;span class="dl"&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 &lt;code&gt;subscribe()&lt;/code&gt; manually and mutate plain fields, you must notify:&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Data&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;/api/data&lt;/span&gt;&lt;span class="dl"&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;d&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&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;cdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markForCheck&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;h3&gt;
  
  
  Router
&lt;/h3&gt;

&lt;p&gt;Router outlet activation and navigations are Angular-controlled, so it should integrate with Zoneless scheduling. The tricky part is often &lt;em&gt;your side effects&lt;/em&gt; (analytics SDKs, third-party callbacks, etc.) that happen during navigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forms
&lt;/h3&gt;

&lt;p&gt;Template-driven and reactive forms are Angular-managed; UI updates should occur.&lt;/p&gt;

&lt;p&gt;Main pitfall: custom controls that update via non-Angular async callbacks must call &lt;code&gt;onChange&lt;/code&gt; correctly and/or &lt;code&gt;markForCheck()&lt;/code&gt; when needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing pitfalls (Zone removal is most visible here)
&lt;/h2&gt;

&lt;p&gt;This is where &lt;strong&gt;angular Zoneless&lt;/strong&gt; tends to surface the most surprises.&lt;/p&gt;

&lt;p&gt;Historically, Angular testing utilities depended on Zone.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fakeAsync()&lt;/code&gt;, &lt;code&gt;tick()&lt;/code&gt;, &lt;code&gt;flush()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waitForAsync()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixture.whenStable()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a fully Zoneless test environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fakeAsync&lt;/code&gt;-style tests can’t work without a zone-like task interceptor.&lt;/li&gt;
&lt;li&gt;Prefer:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;async/await&lt;/code&gt; with explicit timer mocking (Jest/Vitest fake timers), or&lt;/li&gt;
&lt;li&gt;signal-driven tests that don’t require “stability” tracking&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Example with Jest fake timers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updates after timer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useFakeTimers&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;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CounterComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;advanceTimersByTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// explicit&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Count: 1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important pitfall: if you keep Zone.js in tests but remove it in production, &lt;strong&gt;Zone may mask missing &lt;code&gt;markForCheck()&lt;/code&gt; calls&lt;/strong&gt;. Run at least a subset of CI tests zoneless to catch these.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration blueprint: step-by-step (minimize risk)
&lt;/h2&gt;

&lt;p&gt;This is the order I’d recommend if you’re migrating a real production app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Audit dependencies on “incidental ticks”&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search for:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, &lt;code&gt;requestAnimationFrame&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;raw &lt;code&gt;addEventListener&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;WebSocket callbacks&lt;/li&gt;
&lt;li&gt;third-party SDK callbacks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Find places mutating component fields without signals/AsyncPipe/&lt;code&gt;markForCheck&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Adopt explicit reactivity&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert local state to &lt;code&gt;signal()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Convert RxJS subscriptions to &lt;code&gt;AsyncPipe&lt;/code&gt; or &lt;code&gt;toSignal()&lt;/code&gt; when feasible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Standardize manual notification&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For unavoidable imperative updates: end the callback with &lt;code&gt;cdr.markForCheck()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Avoid sprinkling &lt;code&gt;ApplicationRef.tick()&lt;/code&gt; unless you &lt;em&gt;intentionally&lt;/em&gt; want full-app checks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable zoneless change detection&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the Zoneless provider at bootstrap&lt;/li&gt;
&lt;li&gt;Remove Zone.js from polyfills&lt;/li&gt;
&lt;li&gt;Verify: navigation, forms, http, animations (if used)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fix tests&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove zone-based helpers or keep a legacy suite&lt;/li&gt;
&lt;li&gt;Introduce fake timers and explicit &lt;code&gt;detectChanges()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add regression tests around third-party integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Edge cases &amp;amp; gotchas worth calling out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom elements / Web Components&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Events emitted outside Angular template bindings may not mark views dirty unless wired through Angular bindings or paired with &lt;code&gt;markForCheck()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Direct DOM manipulation&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If you update DOM outside Angular templates, Change Detection doesn’t matter.&lt;/li&gt;
&lt;li&gt;But if you update &lt;em&gt;component state&lt;/em&gt; from DOM observers (&lt;code&gt;MutationObserver&lt;/code&gt;, &lt;code&gt;ResizeObserver&lt;/code&gt;), you must notify Angular.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Microtask chains&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;somePromise.then(() =&amp;gt; this.x=1)&lt;/code&gt; won’t auto-tick anymore. Use signals or &lt;code&gt;markForCheck()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Libraries that assume patched globals&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Some older libs may import &lt;code&gt;zone.js&lt;/code&gt; implicitly or depend on zone semantics—validate compatibility.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Performance trap&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Replacing zone-driven global ticks with frequent manual &lt;code&gt;detectChanges()&lt;/code&gt; can be worse.&lt;/li&gt;
&lt;li&gt;Prefer signals / &lt;code&gt;markForCheck()&lt;/code&gt; / AsyncPipe updates (which Angular can coalesce).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




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

&lt;p&gt;The best way to think about &lt;strong&gt;Angular Zoneless&lt;/strong&gt; is: Angular didn’t stop doing change detection—it stopped guessing &lt;em&gt;when&lt;/em&gt; you wanted it.&lt;/p&gt;

&lt;p&gt;Once you migrate the “unknown async” hotspots to signals/AsyncPipe/&lt;code&gt;markForCheck()&lt;/code&gt;, the app tends to become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more predictable&lt;/li&gt;
&lt;li&gt;easier to reason about&lt;/li&gt;
&lt;li&gt;and often faster under load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want, share a snippet of a component that “stops updating” after removing Zone.js (timers, websockets, SDK callbacks, etc.) and I’ll show the smallest zoneless-friendly fix.&lt;/p&gt;

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