<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Unstacked Labs Newsletter]]></title><description><![CDATA[Your technical solutions partner]]></description><link>https://newsletter.unstacked.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!BAnf!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0b14a71-2c8f-42ea-a8a8-8db004cc48a5_419x419.png</url><title>Unstacked Labs Newsletter</title><link>https://newsletter.unstacked.dev</link></image><generator>Substack</generator><lastBuildDate>Thu, 30 Apr 2026 15:46:35 GMT</lastBuildDate><atom:link href="https://newsletter.unstacked.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Unstacked Labs ]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[unstackeddevs@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[unstackeddevs@substack.com]]></itunes:email><itunes:name><![CDATA[Maina Wycliffe]]></itunes:name></itunes:owner><itunes:author><![CDATA[Maina Wycliffe]]></itunes:author><googleplay:owner><![CDATA[unstackeddevs@substack.com]]></googleplay:owner><googleplay:email><![CDATA[unstackeddevs@substack.com]]></googleplay:email><googleplay:author><![CDATA[Maina Wycliffe]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[A Declarative Approach to Building Scalable Applications]]></title><description><![CDATA[Beyond the "How": Why Declarative Architecture is the Secret to Scalable Software]]></description><link>https://newsletter.unstacked.dev/p/a-declarative-approach-to-building</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/a-declarative-approach-to-building</guid><dc:creator><![CDATA[Wayne Gakuo]]></dc:creator><pubDate>Thu, 02 Apr 2026 08:11:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2izS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I began writing software, it never occurred to me that there was something like &#8220;Declarative programming&#8221;, a paradigm that most Software Engineers abide by. As a beginner in the field of programming, you learn the basics &#8212; building blocks of Software Engineering &#8212; and then start applying them in your own projects. As time went by and I became more experienced with my craft, I learned that it&#8217;s not just about learning how to code but also understanding what you&#8217;re building and how to go about doing so.</p><p>As I continued embarking on more projects, working at different companies and working with different clients, I started to understand the meaning of scalable applications; one which can be easily maintained over a period of time while more features are added. This meant that I needed to move towards using a declarative approach that would let me focus less on syntax and more on functionality. Then I bumped into an article that talked about &#8220;Imperative vs Declarative Programming&#8221; and its impact on how we write code.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2izS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2izS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!2izS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!2izS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!2izS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2izS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png" width="1408" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1984156,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/185964434?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2izS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!2izS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!2izS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!2izS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7da072a0-d255-42a2-8c92-4f54f029662a_1408x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>What is Imperative Programming?</h2><p>This is a type of programming paradigm where instructions or commands need to be described explicitly for each step required during execution. With this paradigm, one is more concerned with the &#8220;how&#8221; to get a desired outcome. </p><pre><code>const ages = [10, 20, 30, 40];
const doubledAges = [];

// We are manually managing the loop state and the array mutation
for (let i = 0; i &lt; ages.length; i++) {
  const result = ages[i] * 2;
  doubledAges.push(result);
}

console.log(doubledAges); // [20, 40, 60, 80]
</code></pre><div><hr></div><p>In the code snippet above, you will notice that we are giving the computer a step-by-step recipe. If we forget to initialize the <code>doubledAges</code> array or mess up the loop index, the code fails.</p><p>Let&#8217;s see how this compares to Declarative Programming.</p><h2>What is Declarative Programming?</h2><p>As the name suggests, in this approach we describe what the program does, without explicitly telling it how to do so.</p><pre><code>const ages = [10, 20, 30, 40];

// We describe what we want: a mapped version of the original array
const doubledAges = ages.map(age =&gt; age * 2);

console.log(doubledAges); // [20, 40, 60, 80]</code></pre><div><hr></div><p>In the code snippet above, we don&#8217;t care how the loop works; we just define the transformation from input to output.</p><p></p><p>In the two examples above, both approaches achieve the same result, but they differ on how to go about it. Declarative programming is a layer of abstraction on top of imperative programming. It leverages on the steps already mapped out either using loops and conditionals behind the scenes or by defining an algorithm for the sole purpose of achieving a specific goal. For example, creating utility functions that one can use in their project for specific tasks.</p><h2>Real case scenario: A Real-time Search Input</h2><p>A search bar with real-time suggestions as a user types into it is one of the key features of any e-commerce website today. You would want your users to be able to provide search queries while also getting relevant results quickly. The goal is to:</p><ul><li><p>Listen to user input.</p></li><li><p>Wait for the user to stop typing.</p></li><li><p>Ignore duplicate search terms to avoid redundant queries.</p></li><li><p>Fetch data from the server.</p></li><li><p>Show results only for the latest search request. </p></li></ul><p>Let&#8217;s look at how we would do this with an <strong>imperative approach</strong> (The &#8220;How&#8221;)</p><pre><code>// Component variables
private searchTimeout: any;
private lastSearchTerm = '';
public results = [];

onSearch(event: any) {
  const term = event.target.value;

  // 1. Manually handle debouncing
  clearTimeout(this.searchTimeout);
  this.searchTimeout = setTimeout(() =&gt; {
    
    // 2. Check if the value is actually different
    if (term !== this.lastSearchTerm) {
      this.lastSearchTerm = term;

      // 3. Trigger the API call
      this.searchService.getResults(term).subscribe(data =&gt; {
        this.results = data; // Potential "Race Condition" bug here!
      });
    }
  }, 300);
}</code></pre><div><hr></div><p>In the code above, we are manually managing state variables. This to some extent is a &#8220;fragile&#8221; way because a developer could easily forget to clear a timeout or handle an out-of-order API response. If the first search takes 5 seconds and the second takes 1 second, the first search might overwrite the second search&#8217;s result when it finally arrives, leading to what we call a <strong>race condition</strong>. Manual timeouts and subscriptions can lead to <strong>memory leaks</strong> if the component is destroyed (moving from one page to another).</p><p>Let&#8217;s see how this looks with a <strong>declarative approach</strong> (The &#8220;What&#8221;)</p><pre><code>// Define the "stream" once
readonly searchResults$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),           // Wait for 300ms pause
  distinctUntilChanged(),      // Only act if the text changed
  switchMap(term =&gt;            // Cancel previous request and switch to new one
    this.searchService.getResults(term)
  )
);</code></pre><div><hr></div><p>With the use of libraries such as <strong><a href="https://rxjs-dev.firebaseapp.com/guide/overview">RxJS</a></strong>, we treat the input as a <strong>stream</strong>. We describe the journey of the data rather than the steps that need to be taken along the way.</p><ul><li><p><strong>Self-cleaning</strong>: <code>switchMap</code> automatically cancels the previous HTTP request if a new one starts, hence avoiding any potential race conditions.</p></li><li><p><strong>No state to manage</strong>: We don&#8217;t need <code>lastSearchTerm</code> or <code>searchTimeout</code> variables as the logic is &#8220;pure&#8221;.</p></li><li><p><strong>Async pipe</strong>: By using <code>searchResults$ | async</code> in the template (HTML), Angular handles the subscription &amp; unsubscription automatically.</p></li></ul><h2>Conclusion: Engineering for Longevity</h2><p>The shift from <strong>Imperative</strong> to <strong>Declarative</strong> programming is more than just a technical pivot; it&#8217;s a shift in how we handle complexity. While imperative code might feel faster to write in the short term, it creates a &#8220;hidden tax&#8221; of technical debt, where every new feature increases the risk of side effects and race conditions.</p><p>By adopting a <strong>Declarative Mindset </strong>by leveraging tools like RxJS in Angular, we move away from micro-managing the machine instructions and toward describing the logic of the business.</p><h3>Why this matters for your project:</h3><ul><li><p><strong>Maintainability:</strong> New developers can understand <em>what</em> the system does without deconstructing <em>how</em> every loop is managed.</p></li><li><p><strong>Predictability:</strong> Pure transformations and immutable data flows mean fewer &#8220;it works on my machine&#8221; bugs.</p></li><li><p><strong>Scalability:</strong> Systems built on streams are naturally prepared for the high-concurrency demands of modern enterprise applications.</p></li></ul><p>At <strong>Unstacked Labs</strong>, we don't just ship features; we architect solutions that are resilient to change. Whether we are optimizing an Angular frontend or scaling a backend architecture, our commitment to declarative principles ensures that your codebase remains an asset, not a liability.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Performance: Choosing The Right Tools...]]></title><description><![CDATA[Before I wrote this, I was in the middle of writing a Twitter thread about performance on the web and the unintended impact tools developers choose to build their websites have on their users.]]></description><link>https://newsletter.unstacked.dev/p/performance-choosing-the-right-tools</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/performance-choosing-the-right-tools</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Sun, 14 Sep 2025 10:43:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!BAnf!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0b14a71-2c8f-42ea-a8a8-8db004cc48a5_419x419.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before I wrote this, I was in the middle of writing a Twitter thread about performance on the web and the unintended impact tools developers choose to build their websites have on their users. As I was writing that, I realized that Twitter wasn't probably the most suitable medium to air my thoughts exactly, so I wrote this instead.</p><p>It's pretty common nowadays to find web developers specializing in a single framework. While there is nothing wrong with that, as long as you stick to its strengths, i.e., the right tool for the right job. The saying goes, "when you have a hammer, everything is a nail," and I have seen frontend developers who want to use the only Framework they know for everything, despite it not being the right fit for the task at hand.</p><h2><strong>Some Background</strong></h2><p>Before I can go any further, let me give you some context. I am from Kenya, and most of my friends have $200 or lower smartphones, and about half of those have a $100 or lower smartphone; some don't even have smartphones. This means the devices most of my friends own are quite limited performance-wise. And this is one crucial area we as developers need to understand as we tend to have more powerful devices at our disposal.</p><p>The other thing to be aware of is the state of internet connectivity. It's pretty decent, maybe even good, in some areas, primarily Urban areas, but the speed and reliability fall off the cliff when you start heading to informal settlements and rural areas. This trend can be replicated all over Africa, and I believe in large parts of the world.</p><h2><strong>Performance</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3JlQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3JlQ!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 424w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 848w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 1272w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3JlQ!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif" width="498" height="263" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:263,&quot;width&quot;:498,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!3JlQ!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 424w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 848w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 1272w, https://substackcdn.com/image/fetch/$s_!3JlQ!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bf4e048-0486-4479-8c5b-17d711f1ecbb_498x263.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So, why am I telling you this? When it comes to performance, you need to approach it in two folds: First, the number of KBs the user has to download and the device's capability the user has. The internet speed is impacted by the device in question capabilities, not just the internet coverage. Users with low-end devices tend to also live in areas with poor internet coverage and connectivity.</p><p>The more the KBs your JS script is, the longer it will take to download the script before we can get to parsing, which will also be slower due to device capability limitations. As you can imagine, both of these can take a frustrating longer time on a low-end device on a slow network. When we develop poor-performing websites, i.e., takes longer to load, huge scrips that take a toll on the processor and battery life, we negatively impact our users, especially the already disadvantaged.</p><p>When I am using my iPhone 13 Pro Max (or whatever the latest model is as you are reading this) on the latest 5g network, I will probably notice slow performance, but it will be a minor inconvenience. However, for someone on a $100 or lower smartphone on a slow network, the impact on performance will be huge. We have all been there, trying to load a website that refuses to load or is laggy either due to poor network connection or the toll the website places on both memory and the process. This could mean the difference between someone getting life-saving information in time or not getting it.</p><h3><strong>SPAs</strong></h3><p>One of the most popular ways of developing websites is using Single Page Applications (SPAs) frameworks popularized by React and Angular. These SPA frameworks have become a go-to for developers due to their excellent developer experience and ease of building websites, but this comes at a performance cost. SPA applications need to be bootstrapped, which means you ship your site in JavaScript for the browser to download, parse and render your website in what is known as Client-Side Rendering (CSR).</p><p>So, to recap, the user not only needs to download the Framework Code + your Application code, but they also need the browser to parse it before they can see content on your site. Depending on the size of the Framework and other libraries' sizes, this can quickly balloon to MBs of data.</p><p>Due to the above issues, we have come up with techniques to try and show the content as soon as possible such as Server-Side Rendering (SSR) and Static Site Generation (SSG). But these are not perfect solutions. They both need to put the Framework back into the rendered HTML after the initial content has been displayed for any interactivity, in a process called hydration.</p><p>This is okay (ish) if you are building a web app such as Facebook or YouTube (guess who is behind two of the most used SPA frameworks), where once the site loads, you can access a bazillion of content without having to load the site again. The examples I mentioned above behave more like apps than traditional websites.</p><p>In such instances, I may be okay paying the upfront tax, but subsequent navigations are faster, and requests and data exchange between the server and the client is more efficient. This, of course, is computationally expensive as the Framework in question has to update the DOM to create and removes nodes so that views can change.</p><p>But what if all I wanted to access was information on a single page? Think of a blog post or some instruction on applying for a government grant or social benefits. Do I have to download and run a whole web app to access this information?</p><p>As you can imagine, this can be very painful, especially for government sites, NGOs, and business services whose customer base has users with poor internet coverage and low-end devices or a combination of both.</p><p>To be honest, this should extend to everyone, no matter the internet connection speed or kind of device I have; I shouldn't have to download tons of JS code just to read your blog post.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eGdo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eGdo!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 424w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 848w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 1272w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eGdo!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif" width="320" height="314.4347826086956" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:226,&quot;width&quot;:230,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!eGdo!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 424w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 848w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 1272w, https://substackcdn.com/image/fetch/$s_!eGdo!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63bff7b6-e7c1-4505-b5ec-bb1a48cd8b61_230x226.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2><strong>In Conclusion</strong></h2><p>So, where am I going with this? The title of this article is "The Right Tool for the Right Job." I mean that as developers, we need to think about our users and the purpose of what we are building before choosing the tools to use. Whatever tools and frameworks we use, we need to emphasize more on the user experience we are providing. This means that we need to learn to have more than one tool in our toolkit and be open to learning more. Lately, we have seen an uptick in frameworks and libraries geared towards 0KB Javascript without sacrificing developer experience. I believe every front-end developer should know at least one.</p><p>We have a lot of tools at our disposal from SPAs frameworks such as Angular, React, Vue, etc., which have their use cases but should not be used for everything. On the other hand, we have frameworks and tools for building Multi-Page Applications (MPAs) and Static Sites, such as Astro, Hugo, Jekyll, etc., that are designed for shipping as little Javascript as possible while providing incredible speeds most SPAs frameworks can only dream off.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9FZw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9FZw!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 424w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 848w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 1272w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9FZw!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif" width="498" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:498,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!9FZw!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 424w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 848w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 1272w, https://substackcdn.com/image/fetch/$s_!9FZw!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F01dcc8f5-129f-447c-b743-e8ceab022098_498x280.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My point is as a developer; you should make sure you have a few of these tools in your tool kit; this will ensure that whenever you are choosing, you choose the right tool for the job.</p>]]></content:encoded></item><item><title><![CDATA[Island Architecture Explained]]></title><description><![CDATA[In this post, we explore what island architecture is in simple terms and why alot of frameworks are adopting it]]></description><link>https://newsletter.unstacked.dev/p/island-architecture-explained</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/island-architecture-explained</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Sun, 14 Sep 2025 10:38:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!BAnf!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0b14a71-2c8f-42ea-a8a8-8db004cc48a5_419x419.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are different ways to build a website. One of them was Multi-Page Applications (MPAs) which went out of fashion about a decade ago and are having a comeback. MPAs had been replaced by the Single Page Applications (SPAs) approach popularized by Angular and React, among other modern frameworks.</p><p>Due to how trends work, it is easy for methods/tools to go out of fashion. This is not because it is less effective but because developers stop using them in favor of something else. This is what happened with Multi-Page Applications (MPAs), as developers started adopting popular web frameworks such as Angular, React, Vue, etc. This led to an uptick in SPA usage as the frameworks became more popular.</p><p>Due to how SPAs work, the impact has been that the amount of Javascript we ship to the browser has increased. i.e., you can't have a react web app without React to manage state and rendering. SPAs use Javascript to render the HTML to be displayed in the browser. In many cases, this makes sense, for instance, in web apps such as Facebook or YouTube, where managing the state is paramount. But in other cases, this does not make sense, for example, blogs, personal portfolios, etc.</p><h2><strong>Server-Side Rendering</strong></h2><p>When using a framework such as Angular or React and SPAs in general, the server does very little; all the rendering is done on the Client-Side, in what is known as Client-Side Rendering. To see the content, you first have to download the framework's runtime (JS needed to scaffold your web app) and also need an environment to render the content, i.e., the browser.</p><p>This has a few downsides, the notable ones being it's slow to show something on the screen, the impact of this is worse on low-end devices and slower internet connection and Search Engine Optimization - bots and crawlers are usually unable to render these pages and can't parse the content to show results.</p><p>We have two standard solutions to solve the above problems: Server-Side Rendering (SSR) and Rendering during build time - SSG. SSG is similar to SSR but at build time, removing the need to render on every request at the server. SSG is common for sites whose content isn't that dynamic. The problem with these two solutions is that they don't solve the problem with SPAs but rather postpone it.</p><p>If you want any sort of interactivity, say opening and closing the navbar on your web app, you will need to hydrate your rendered app from either SSG or SSR. This is the process of bootstrapping the framework you are using, transferring the state from the server so that the framework can take over. This usually happens after the first content is painted (after rendering the server-side rendered HTML from the server) but before the interactivity in your web app.</p><p>This means the JS needed by your framework has to be downloaded and parsed, and the user has to wait for all that to happen to interact with your web app. This would mean even on pages where you don't need interactivity, i.e., About Us Page, Terms and Conditions, etc., you still need to do all that, which is a bit unnecessary.</p><h2><strong>Islands</strong></h2><p>And this is where Islands Architecture comes in. Imagine this; what if you could create your web app with pure HTML and CSS for all the static content but then add in regions of dynamic content or interactivities - islands - that can use a framework to add interactivity. These regions would use any framework, and the framework runtime would only be downloaded only when on a page that uses it rather than on the initial load of the website.</p><p>Web frameworks such as Astro (<a href="https://mainawycliffe.dev/?ref=content.mainawycliffe.dev">My website</a> is built with Astro), Marko, and most recently Qwik, among others, are implementing this architecture method. In the case of Astro, you have Astro components that use some variation of JSX but do not have a client-side state, so there is no runtime.</p><h3><strong>Astro</strong></h3><p>Astro uses the concept of JS opt-in, meaning by default, no Javascript is generated unless you tell Astro to include javascript. You can then either use Vanilla JS to include Javascript - the old fashioned way, as shown below:</p><blockquote><p>Astro components cannot be hydrated as they are HTML only templating components and any Javascript needs to be included as indicated above or via a framework such as React, SolidJS, etc.</p></blockquote><p>The second option is to bring your framework, for example, React, Preact, Lit, Svelte, Vue, etc., to create components that add regions of interactivity (islands) in your web app.</p><pre><code><code>// index.astro file
---
import ReactComponent from "./ReactComponent"

---

&lt;ReactComponent /&gt;</code></code></pre><p>You are also in control of when the necessary region is hydrated. This is done via directives that instruct Astro when to perform hydration. For instance, you might want an island to be hydrated on load or only when it becomes visible. There are several directives to help you achieve this, which you can learn more about <a href="https://docs.astro.build/en/core-concepts/component-hydration/?ref=content.mainawycliffe.dev">here</a>.</p><h3><strong>Marko and Qwik</strong></h3><p>While I am not an expert at either Marko.js or Qwik (the new kid in the block), I will link additional resources below if you are interested in learning more. Marko and Qwik take the concept of islands a little further when compared to Astro.</p><p>Marko is an MPA framework, and its Island architecture is a bit smarter in that it automatically decides to load JS needed for an Island, delaying it as far as possible, allowing for far more efficient islands. This is unlike the current Astro approach, which relies on the developer to tell Astro when to do hydration. Astro is still in the pre-release stage, and maybe this will change in the future.</p><p>Another key advantage Marko has over Astro is that Marko decides what is inside the Islands and what's not in it. This means components such as footers, headers, etc., that only display static content don't become islands, while forms and other rich components with dynamic content become islands that can be hydrated.</p><p>Qwik, on the other hand, takes this to a component level, breaking down how hydration is done so that it is done only when needed. This is achieved by aggressively breaking apart your website's JavaScript into multiple chunks, setting up global event listeners, and serializing points of interest directly into the HTML. For each distinct user interaction, Qwik has all it needs to load only the code required to perform the action and nothing more.</p><p></p><p>In return, this leads to smaller chunks, which are faster to load, parse and load only what the user needs. This is known as <a href="https://www.builder.io/blog/why-progressive-hydration-is-harder-than-you-think?utm_source=twitter">progressive hydration</a>, which is out of scope for this article, and hopefully, I will write about it soon.</p><h2><strong>Conclusion</strong></h2><p>This article looked at Islands Architecture, why they exist, and the frameworks currently applying them. In the next series of articles, I want to dig deeper into the frameworks mentioned above - Astro, Marko, and Qwik, plus other frameworks such as Svelte, Angular, and React and how they differ internally from each other.</p><h2><strong>Resources</strong></h2><ol><li><p><a href="https://www.builder.io/blog/why-progressive-hydration-is-harder-than-you-think?utm_source=twitter">Why Progressive Hydration is Harder than You Think</a></p></li><li><p><a href="https://www.builder.io/blog/from-static-to-interactive-why-resumability-is-the-best-alternative-to-hydration?ref=content.mainawycliffe.dev">From Static to Interactive: Why Resumability is the Best Alternative to Hydration</a></p></li><li><p><a href="https://dev.to/this-is-learning/javascript-vs-javascript-fight-53fa?ref=content.mainawycliffe.dev">JavaScript vs. JavaScript. Fight!</a></p></li><li><p><a href="https://dev.to/this-is-learning/why-efficient-hydration-in-javascript-frameworks-is-so-challenging-1ca3?ref=content.mainawycliffe.dev">Why Efficient Hydration in JavaScript Frameworks is so Challenging</a></p></li><li><p><a href="https://dev.to/this-is-learning/resumable-javascript-with-qwik-2i29?ref=content.mainawycliffe.dev">Resumable JavaScript with Qwik</a></p></li><li><p><a href="https://dev.to/ryansolid/comment/1ni8p?ref=content.mainawycliffe.dev">Conquering JavaScript Hydration Event delegation is the key to not running over the component... Apr 15</a></p></li><li><p><a href="https://dev.to/ryansolid/state-of-javascript-2021-framework-reflections-2i77?ref=content.mainawycliffe.dev">State of JavaScript 2021: Framework Reflections</a></p></li><li><p><a href="https://www.builder.io/blog/introducing-qwik-framework?ref=content.mainawycliffe.dev">A first look at Qwik - the HTML first framework WRITTEN BYMI&#352;KO HEVERY JULY 2, 2021</a></p></li></ol>]]></content:encoded></item><item><title><![CDATA[Beyond the Binge: Building Smart Movie & TV Recommendations with Angular, Genkit & Firebase]]></title><description><![CDATA[The Dawn of Personalized Entertainment: Building an AI-Powered Recommender]]></description><link>https://newsletter.unstacked.dev/p/beyond-the-binge-building-smart-movie</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/beyond-the-binge-building-smart-movie</guid><dc:creator><![CDATA[Wayne Gakuo]]></dc:creator><pubDate>Mon, 11 Aug 2025 11:57:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Rirn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ever wondered how streaming platforms magically suggest the perfect movie or TV show, making it seem like they know your tastes better than you do? Today, we're pulling back the curtain on that magic! We're diving into the exciting world of building an intelligent recommendation engine for movies and TV shows, designed to understand your unique preferences and offer truly personalized viewing suggestions.</p><p>This journey will take us through leveraging the power of <strong><a href="https://firebase.google.com/">Firebase</a></strong>, the rich data of the <strong><a href="https://developer.themoviedb.org/docs/getting-started">TMDB API</a></strong>, and the cutting-edge capabilities of Google's <strong><a href="https://ai.google.dev/gemini-api/docs/models">Gemini models</a></strong> orchestrated by the <strong><a href="https://genkit.dev/">Genkit framework</a></strong>. We'll explore a practical, secure, and scalable architectural approach, showcasing how these powerful technologies integrate seamlessly to enhance user engagement and transform your application into a truly smart entertainment hub.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><strong>An AI-powered recommendation engine, powered by Genkit</strong></h2><p>Our fictional movie and TV shows hub, &#8220;Nova Reel&#8221;, is an <strong><a href="https://angular.dev/">Angular-based</a></strong> application for browsing, discovering, and getting personalized recommendations for movies and TV shows. It leverages Google's Genkit AI platform to provide intelligent recommendations based on user favorites.</p><p>Nova Reel app:  https://nova-reels.web.app/</p><p>GitHub Repo: https://github.com/waynegakuo/nova-reel</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Rirn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Rirn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 424w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 848w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 1272w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Rirn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png" width="1456" height="741" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07e60a93-b37c-4317-a248-daddc6420596_1683x857.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:741,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:715866,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169210266?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Rirn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 424w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 848w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 1272w, https://substackcdn.com/image/fetch/$s_!Rirn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07e60a93-b37c-4317-a248-daddc6420596_1683x857.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>                                         Screenshot of the Nova Reel web application</em></p><h3><strong>Key Features</strong></h3><ul><li><p>&#128293; Browse trending movies and TV shows</p></li><li><p>&#128269; Filter content by categories (Popular, Top Rated, Now Playing, etc.)</p></li><li><p>&#128270; Search for specific titles</p></li><li><p>&#8505;&#65039; View detailed information about movies and TV shows</p></li><li><p>&#11088; Save favorites for quick access</p></li><li><p>&#129302; Get AI-powered personalized recommendations based on your favorites</p></li><li><p>&#128241; Responsive design for all devices</p></li></ul><h3><strong>&#127959;&#65039; Architecture</strong></h3><p>Nova Reel is built with a modern tech stack that combines frontend and backend technologies:</p><h4><strong>&#128421;&#65039; Frontend</strong></h4><ul><li><p><strong>&#127344;&#65039; Angular</strong>: The frontend is built with Angular, using standalone components and signals for reactive state management</p></li><li><p><strong>&#128272; Firebase Authentication</strong>: For user authentication and management</p></li><li><p><strong>&#128452;&#65039; Firebase Firestore</strong>: For storing user favorites and preferences</p></li><li><p><strong>&#128293; Angular Fire</strong>: For integrating Firebase services with Angular</p></li></ul><h4><strong>&#9729;&#65039; Backend</strong></h4><ul><li><p><strong>&#9889; Firebase Functions</strong>: Serverless backend functions that handle API requests and AI processing</p></li><li><p><strong>&#129504; Genkit</strong>: Google's AI platform for building generative AI applications</p></li><li><p><strong>&#128171; Gemini 2.5 Pro</strong>: The underlying AI model used for generating recommendations</p></li><li><p><strong>&#127902;&#65039; TMDB API</strong>: External API for fetching movie and TV show data</p></li></ul><h4><strong>&#129302; AI Recommendation Engine</strong></h4><p>The heart of Nova Reel is its AI recommendation engine, which uses Genkit to analyze user favorites and generate personalized recommendations:</p><ol><li><p><strong>&#128202; Data Collection</strong>: The app stores user favorites in Firebase Firestore</p></li><li><p><strong>&#129504; AI Processing</strong>: When a user requests recommendations, the app calls a Firebase Function that uses Genkit to:</p><ul><li><p>&#128229; Fetch the user's favorites from Firestore</p></li><li><p>&#128260; Create a context from these favorites to inform the AI</p></li><li><p>&#128171; Use the Gemini 2.5 Pro model to generate personalized recommendations</p></li><li><p>&#128161; Provide reasoning for the recommendations</p></li></ul></li><li><p><strong>&#128241; Recommendation Display</strong>: The frontend displays these recommendations in the "For You" tab, along with the AI's reasoning</p></li></ol><h3><strong>Why use Genkit?</strong></h3><ol><li><p><strong>Smart Data Integration (Retrieval Augmented Generation&#8212;RAG): </strong>the AI fetches your favorites from Firestore, then dynamically queries  TMDB as it &#8220;thinks&#8221; using our <code>getTmdbDataTool</code>. This grounds the AI in your real data, leading to <strong>highly relevant recommendations</strong>.</p></li><li><p><strong>AI-Driven Actions (Tool Calling):</strong> Our AI uses the <code>getTmdbDataTool </code>to fetch data from TMDB. It&#8217;s like giving the AI its own web browser and search engine. This empowers the AI to reason and retrieve information for better suggestions.</p></li><li><p><strong>Robust Backend &amp; Security:</strong> Our AI logic runs on Firebase Cloud Functions.</p><ol><li><p><strong>Secure API Keys:</strong> TMBD &amp; Gemini keys stay safely hidden on the server.</p></li><li><p><strong>Complex logic offloaded:</strong> Keeps the heavy lifting off the user&#8217;s device.</p></li></ol></li><li><p>Debugging &amp; Observability: Genkit comes with a <strong>Developer UI</strong> and <strong>Firestore tracing</strong>. We can literally see:</p><ol><li><p>What prompt did the AI receive?</p></li><li><p>What &#8220;tools&#8221; it decided to call.</p></li><li><p>The exact data returned by those tools.</p></li><li><p>The AI&#8217;s final reasoning.</p></li></ol></li></ol><blockquote><p><strong>NB:</strong> This guide assumes you have Angular CLI (v19 or later), Node.js (v20+), npm, and Git installed. If not, kindly check out the <a href="https://github.com/waynegakuo/nova-reel?tab=readme-ov-file#-setting-up-locally-prerequisites">GitHub Repo</a> for the requirements for this project.</p></blockquote><h3><strong>&#128293; Firebase Project Setup &amp; TMDB API Key</strong></h3><h4><strong>&#127959;&#65039; Create a Firebase Project:</strong></h4><ol><li><p>Go to <a href="https://console.firebase.google.com/">Firebase Console</a>.</p></li><li><p>Click "Add project" and follow the prompts to create your project.</p></li><li><p>&#9888;&#65039; <strong>Important:</strong> Upgrade your project to the Blaze (pay-as-you-go) plan. Cloud Functions and Vertex AI (which Genkit uses) require a billing-enabled project. Don't worry, free tiers are generous for testing.</p></li></ol><p></p><h4><strong>&#9729;&#65039; Enable Essential Google Cloud APIs</strong></h4><p>Your Firebase project uses Google Cloud behind the scenes. For secure secret management, the Secret Manager API must be enabled. Other necessary APIs (like Cloud Functions, Cloud Build, Cloud Run, Vertex AI) are usually enabled automatically by Firebase when you deploy functions or use AI features.</p><ul><li><p>Go to the Google Cloud Console for your Firebase project.</p></li><li><p>In the navigation menu, go to APIs &amp; Services &gt; Enabled APIs &amp; Services.</p></li><li><p>Click on <strong>+Enable APIs and services</strong>.</p></li><li><p>Search for and enable the Secret Manager API.</p></li></ul><p></p><h4><strong>&#128736;&#65039; Install Firebase CLI:</strong></h4><blockquote><p><strong>Note for Firebase Studio users:</strong> Skip step 2 (npm install command) and go directly to step 3 (firebase login).</p></blockquote><ol><li><p>Open your terminal/command prompt.</p></li><li><p>Install the Firebase CLI globally:</p></li></ol><pre><code><code>npm install -g firebase-tools
</code></code></pre><ol><li><p>Log in to Firebase:</p></li></ol><pre><code><code>firebase login</code></code></pre><p></p><h4><strong>&#128640; Initialize Firebase in Your Project:</strong></h4><ol><li><p>Navigate to your project's root directory (you should already be there after cloning the repository).</p></li><li><p>Initialize Firebase:</p></li></ol><pre><code><code>firebase init</code></code></pre><ol start="3"><li><p>Select "Functions" and "Firestore" when prompted.</p></li><li><p>Choose your existing Firebase project to link to.</p></li><li><p>Select TypeScript for functions (highly recommended).</p></li><li><p>For Firestore, accept the default rules file (you can change it later).</p></li><li><p>Do NOT overwrite existing files if prompted.</p><p></p></li></ol><h4><strong>&#128260; Update the .firebaserc File:</strong></h4><p>You can update the .firebaserc file in two ways:</p><h5><strong>Option 1: Using Firebase CLI (Recommended)</strong></h5><p>Run the following command to set your Firebase project ID:</p><pre><code><code>firebase use YOUR_PROJECT_ID</code></code></pre><p>Replace <code>YOUR_PROJECT_ID</code> with the project ID of the Firebase project you created. This command will automatically update your .firebaserc file.</p><h5><strong>Option 2: Manual Editing</strong></h5><ol><li><p>Open the <code>.firebaserc</code> file in your project root directory.</p></li><li><p>Replace the default project name with your Firebase project ID:</p></li></ol><pre><code>{
  "projects": {
    "default": "YOUR_PROJECT_ID"
  }
}</code></pre><p>Replace <code>YOUR_PROJECT_ID</code> with the project ID of the Firebase project you created.</p><h4><strong>&#128293; Add Firebase Configuration to Your Project:</strong></h4><ol><li><p>Go to your Firebase project in the <a href="https://console.firebase.google.com/">Firebase Console</a>.</p></li><li><p>Click on the gear icon (&#9881;&#65039;) next to "Project Overview" and select "Project settings".</p></li><li><p>Scroll down to the "Your apps" section and select your web app (or create one if you haven't already).</p></li><li><p>Under the "SDK setup and configuration" section, select "Config" to view your Firebase configuration object.</p></li><li><p>Copy the configuration object that looks like this:</p></li></ol><pre><code>{
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
  measurementId: "YOUR_MEASUREMENT_ID"
}</code></pre><ol start="6"><li><p>Open the environment files in your project:</p></li></ol><ul><li><p>For production: <code>src/environments/environment.ts</code></p></li><li><p>For development: <code>src/environments/environment.development.ts</code></p></li></ul><ol start="7"><li><p>Replace the existing <code>firebaseConfig</code> object with your own Firebase configuration:</p></li></ol><pre><code>export const environment = {
  production: true, // or false for environment.development.ts
  firebaseConfig: {
    apiKey: "YOUR_API_KEY",
    authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
    projectId: "YOUR_PROJECT_ID",
    storageBucket: "YOUR_PROJECT_ID.appspot.com",
    messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
    appId: "YOUR_APP_ID",
    measurementId: "YOUR_MEASUREMENT_ID"
  }
};</code></pre><h4><strong>&#127916; Get TMDB API Key:</strong></h4><ol><li><p>Go to <a href="https://www.themoviedb.org/">TMDB</a>.</p></li><li><p>Sign up or log in.</p></li><li><p>Go to your user profile (click your avatar) -&gt; Settings -&gt; API.</p></li><li><p>Request a new API key (Developer/v3).</p></li><li><p>Note down your API Read Access Token (Bearer Token). It starts with "eyJ...".</p><p></p></li></ol><h4><strong>&#128272; Set TMDB API Key as Firebase Secret:</strong></h4><ol><li><p>Navigate to your functions directory:</p></li></ol><pre><code><code>cd functions</code></code></pre><ol start="2"><li><p>Set the TMDB API key as a Firebase secret:</p></li></ol><pre><code><code>firebase functions:secrets:set TMDB_API_BEARER_TOKEN</code></code></pre><ol start="3"><li><p>Paste your TMDB Bearer Token when prompted.</p></li><li><p>Return to the project root:</p></li></ol><pre><code><code>cd ..</code></code></pre><h4><strong>&#128273; API Keys and Deployment</strong></h4><ol><li><p>&#128273; Set up your API keys:</p></li></ol><ul><li><p>Create a <code>.env</code> file in the <code>functions</code> directory with your Gemini API key (for local development):</p></li></ul><pre><code><code>cd functions
echo "GEMINI_API_KEY=your_gemini_api_key" &gt; .env
cd ..</code></code></pre><ul><li><p>Set up the Gemini API key as a Firebase secret (for production deployment):</p></li></ul><pre><code><code>firebase functions:secrets:set GEMINI_API_KEY</code></code></pre><blockquote><p><strong>Note:</strong> When running this command, you'll be prompted to enter the actual secret value. The GEMINI_API_KEY is needed both as an environment variable (for local development) and as a Firebase secret (for production deployment).</p></blockquote><ol start="2"><li><p>&#128293; Configure Firebase:</p></li></ol><pre><code><code>firebase use your-project-id</code></code></pre><ol start="3"><li><p>&#128640; Deploy Firebase Functions:</p></li></ol><pre><code><code>firebase deploy --only functions</code></code></pre><ol start="4"><li><p>&#127939;&#8205;&#9794;&#65039; Run the application locally:</p></li></ol><pre><code><code>ng serve</code></code></pre><h2><strong>&#129504; Building an AI Recommendation Engine with Genkit</strong></h2><p>Nova Reel demonstrates how to build an AI recommendation engine using Genkit. Here's how it works:</p><h3><strong>1&#65039;&#8419; Setting Up Genkit</strong></h3><p>In the Firebase Functions (<code>functions/src/index.ts</code>), Genkit is configured with the Gemini model:</p><pre><code>// Configure Genkit
const ai = genkit({
  plugins: [
    googleAI({apiKey: process.env.GEMINI_API_KEY }),
  ],
  model: googleAI.model('gemini-2.5-pro'), // Specify your Gemini model
});</code></pre><h3><strong>2&#65039;&#8419; Creating a Custom Tool for TMDB Data</strong></h3><p>A custom tool is defined to allow the AI to fetch movie and TV show data from TMDB:</p><pre><code>export const getTmdbDataTool = ai.defineTool(
  {
    name: 'getTmdbData',
    description: 'Fetches movie or TV show data from TMDB using specific endpoint, ID, or query.',
    inputSchema: z.object({
      endpoint: z.string().describe('TMDB API endpoint (e.g., "movie", "tv", "search/movie")'),
      id: z.number().optional().describe('ID for specific movie/TV show'),
      query: z.string().optional().describe('Search query string'),
    }),
    outputSchema: z.any().describe('JSON data from TMDB API response'),
  },
  async ({ endpoint, id, query }) =&gt; {
    const url = constructTmdbUrl(endpoint, { id, query });
    return await executeTmdbRequest(url, 'getTmdbDataTool');
  }
);</code></pre><h3><strong>3&#65039;&#8419; Defining the Recommendation Flow</strong></h3><p>A Genkit flow is defined to handle the recommendation process:</p><pre><code>export const _getRecommendationsFlowLogic = ai.defineFlow(
  {
    name: 'getRecommendationsFlow',
    inputSchema: RecommendationInputSchema,
    outputSchema: RecommendationOutputSchema,
  },
  async (input) =&gt; {
    // Fetch user's favorite movies/tv shows from Firestore
    const userFavoritesRef = db.collection('users').doc(input.userId).collection('favorites');
    const favoritesSnapshot = await userFavoritesRef.get();
    const favoriteItems = favoritesSnapshot.docs.map(doc =&gt; doc.data());

    // Prepare context for the AI from user favorites
    let favoritesContext = favoriteItems.map(item =&gt;
      `${item['type'] === 'movie' ? 'Movie' : 'TV Show'}: ${item['title']} (TMDB ID: ${item['tmdbId']})`
    ).join('; ');

    // Generate recommendations using the AI model
    const { output } = await ai.generate({
      tools: [getTmdbDataTool], // Make the TMDB data tool available to the model
      prompt: `
        You are a highly intelligent movie and TV show recommendation assistant.
        The user has a list of favorite movies and TV shows.
        User's favorites: ${favoritesContext}

        Based on these favorites, recommend ${input.count} additional movies or TV shows that the user might enjoy.
        Consider their genres, actors, directors, themes, and overall vibe.
        Prioritize items with high TMDB ratings.
        Avoid recommending any of the items already in the user's favorites list.
        
        For each recommendation, provide the title, whether it's a "movie" or "tv" show, its TMDB ID, 
        a brief overview, and its poster path.
        
        Explain your reasoning briefly after the recommendations.
      `,
      output: {
        format: 'json',
        schema: RecommendationOutputSchema,
      },
    });

    return output || { recommendations: [], reasoning: 'Unable to generate recommendations.' };
  }
);</code></pre><h3><strong>4&#65039;&#8419; Exposing the Flow as a Firebase Function</strong></h3><p>The flow is exposed as a callable Firebase Function:</p><pre><code>export const getRecommendationsFlow = onCallGenkit(
  {
    secrets: [TMDB_BEARER_TOKEN],
    region: 'africa-south1',
    cors: true,
  },
  _getRecommendationsFlowLogic
);</code></pre><h3><strong>5&#65039;&#8419; Calling the Function from the Frontend</strong></h3><p>In the frontend (<code>src/app/services/media/media.service.ts</code>), the function is called to get recommendations:</p><pre><code>async getAiRecommendationsData(count: number = 5): Promise&lt;AiRecommendationResponse&gt; {
  const userId = this.authService.getUserId();
  if (!userId) {
    throw new Error('User must be authenticated to get AI recommendations');
  }

  const callableGetRecommendations = httpsCallable(this.functions, 'getRecommendationsFlow');
  try {
    const result = await callableGetRecommendations({
      userId: userId,
      count: count
    });
    return result.data as AiRecommendationResponse;
  } catch (error: any) {
    console.error('Error fetching AI recommendations from Cloud Function:', error);
    throw error;
  }
}</code></pre><h3><strong>6&#65039;&#8419; Displaying Recommendations</strong></h3><p>The recommendations are displayed in the "For You" tab of the landing page, along with the AI's reasoning for the recommendations.</p><h2><strong>&#128221; Usage</strong></h2><ol><li><p><strong>&#128269; Browse Content</strong>: Use the "Movies" and "TV Shows" tabs to browse trending content</p></li><li><p><strong>&#8505;&#65039; View Details</strong>: Click on any movie or TV show to view detailed information</p></li><li><p><strong>&#11088; Add to Favorites</strong>: Click the "Add to Favorites" button on any movie or TV show detail page</p></li><li><p><strong>&#129302; Get Recommendations</strong>: Navigate to the "For You" tab to see personalized recommendations based on your favorites</p></li><li><p><strong>&#128260; Refresh Recommendations</strong>: Click the "Refresh Recommendations" button to get new recommendations</p></li></ol><h2><strong>Next Steps: A Series of Smart AI Features</strong></h2><p>This recommendation engine is just the beginning. Our architecture, built on Genkit, provides a flexible foundation for a series of new AI-powered features. As a next step, we will continue this journey by:</p><ol><li><p><strong>"Guess the Movie" from a Screenshot:</strong> We'll build a feature that allows users to upload a screenshot of a movie scene. Our AI agent, using a vision-enabled Gemini model, will analyze the image to identify the title, which will then be verified with the TMDB API to provide accurate information.</p></li><li><p><strong>Mood-Based Recommendations:</strong> We'll introduce a feature that lets users dictate their mood (e.g., "I want a thrilling, suspenseful movie") in natural language. Our AI will understand the sentiment and keywords to recommend movies that match the user's emotional state, moving beyond simple genre filtering.</p></li><li><p><strong>An Interactive Chatbot:</strong> We will create a conversational agent that can handle multi-turn requests. Users can ask questions like "Who directed this movie?" or give commands like "Add this to my watchlist," and the AI will use its tools to perform these actions in a conversational manner.</p></li></ol><p>By adding these features, we can turn a simple recommendation app into a much more dynamic and user-friendly experience, with Genkit acting as the intelligent layer that makes it all possible. This approach demonstrates how AI can be a central part of an application's architecture, not just a supporting tool.</p><h2><strong>Conclusion</strong></h2><p>Personalized recommendation engines have evolved from a novelty to an essential part of the modern digital experience. This article has showcased a practical approach to building a robust and intelligent recommender system for movies and TV shows. By leveraging <strong>Firebase Cloud Functions</strong>, we can securely handle API keys and offload complex logic from the frontend. Using <strong>Firestore</strong>, we efficiently store and retrieve user-specific data, forming the basis for personalization. Finally, by integrating the <strong>Genkit framework</strong>, we empower the <strong>Google Gemini model</strong> to act as a data-aware AI agent.</p><p>This architecture allows the AI to autonomously "reason" and "act" by calling a tool that connects to the <strong>TMDB API</strong>, enabling it to fetch real-time data to ground its recommendations. This multi-step, server-side process ensures the suggestions are not only personalized but also accurate and reliable. The result is a seamless and engaging user experience that goes beyond simple content matching.</p><p>The ongoing advancements in AI promise even greater innovation in this field, with future systems likely to incorporate more explainable AI for transparency, federated learning for privacy, and real-time adaptations to user behavior. The pursuit of creating a frictionless and truly tailored entertainment experience will continue to be a driving force, with AI playing an increasingly central role.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Dev 101: TypeScript 5.0+ Decorators]]></title><description><![CDATA[The Decorator Pattern in Typescript 5.0 and above features a new API while still allowing to enjoy attaching new behaviors to objects by placing them inside special wrapper objects, called decorators. This allows you to add functionality without modifying the original class.]]></description><link>https://newsletter.unstacked.dev/p/dev-101-typescript-50-decorators</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/dev-101-typescript-50-decorators</guid><dc:creator><![CDATA[Alex Muturi]]></dc:creator><pubDate>Fri, 01 Aug 2025 12:08:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/340afe04-1083-4915-97a9-3e051fe2db39_626x348.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div><hr></div><h2><strong>1. Foundation: The Decorator Pattern</strong></h2><p>The <strong>Decorator Pattern</strong> is a structural design pattern that attaches new behaviors to objects by placing them inside special wrapper objects, called decorators. This allows you to add functionality without modifying the original class. Go straight to the code [<a href="https://github.com/alex-migwi/angular-decorators-NG-Kenya-2025">github repo</a>]</p><pre><code><code>// Classical Decorator Pattern Example
class Coffee {
  cost() { return 5; }
}
class MilkDecorator {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1; }
}
const milkCoffee = new MilkDecorator(new Coffee());
console.log(milkCoffee.cost()); // 6
</code></code></pre><p>TypeScript 5.0+ decorators are inspired by this concept: they let you <strong>add behavior</strong> or <strong>metadata</strong> to classes, methods, and properties in a declarative, compile-time manner.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><blockquote><p>In Typescript versions lesser than 5.0, decorators have a different implementation. Here is the link to an article covering lower versions: <a href="https://newsletter.unstacked.dev/p/dev-101-typescript-legacy-pre-v50">Typescript Decorators (Pre-v5.0)</a></p><p>TypeScript for a while had support for "<strong>experimental</strong>" decorators for years. While these experimental decorators have been incredibly useful, they modeled a much older version of the decorators proposal, and always required an opt-in compiler flag called <code>--experimentalDecorators</code>. Any attempt to use decorators in TypeScript without this flag used to prompt an error message.</p><p>According to this <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators">announcement</a>, <code>--experimentalDecorators</code> will continue to exist for the foreseeable future; however, without the flag, decorators will now be valid syntax for all new code. Outside of <code>--experimentalDecorators</code>, they will be type-checked and emitted differently. The type-checking rules and emit are sufficiently different that while decorators <em>can</em> be written to support both the old and new decorators behavior, any existing decorator functions are not likely to do so.</p></blockquote><div><hr></div><h2><strong>2. TypeScript 5.0+ Decorators: Basics &amp; Parameters</strong></h2><p>To use the new decorator API in TypeScript 5.0+, configure your <code>tsconfig.json</code>:</p><pre><code><code>{
  "target": "ESNext",
  "module": "ESNext",
  "experimentalDecorators": true,
  "useDefineForClassFields": true
}
</code></code></pre><h3><strong>2.1 Types of Decorators &amp; Their Parameters</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6LbG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6LbG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 424w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 848w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 1272w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6LbG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png" width="955" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/acbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:955,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25764,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169727752?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6LbG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 424w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 848w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 1272w, https://substackcdn.com/image/fetch/$s_!6LbG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facbc7f47-0040-4eaa-8274-c8e29359b005_955x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4><strong>Parameter Details</strong></h4><ul><li><p><strong>Class Decorator</strong>: Receives the class constructor and returns it (or a new constructor).</p></li><li><p><strong>Field Decorator</strong>: Receives a context object and returns a function that processes the field value.</p></li><li><p><strong>Method Decorator</strong>: Receives a context object and can use <code>addInitializer</code> to modify the method.</p></li><li><p><strong>Accessor Decorator</strong>: Receives a context object for getter/setter methods.</p></li></ul><pre><code><code>// New API Examples
function MyClassDecorator&lt;T extends new (...args: any[]) =&gt; any&gt;(constructor: T): T {
  console.log('Class created:', constructor.name);
  return constructor;
}

function MyFieldDecorator&lt;T extends object, K extends keyof T&gt;(
  target: undefined, 
  context: ClassFieldDecoratorContext&lt;T, T[K]&gt;
): (value: T[K]) =&gt; T[K] {
  return (value: T[K]) =&gt; value;
}

function MyMethodDecorator&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  context.addInitializer(function (this: T) {
    // Modify the method here
  });
}
</code></code></pre><p><strong>Example Recap:</strong></p><pre><code><code>class Example {
  @MyFieldDecorator
  someProperty: string = "hello";

  @MyMethodDecorator
  someMethod(msg: string) {}
}
</code></code></pre><blockquote><p>Decorators execute at <strong>compile/design time</strong>. You can modify behavior or attach annotations used by frameworks.</p></blockquote><div><hr></div><h2><strong>3. Reflection &amp; Metadata</strong></h2><p>To read design-time types at runtime, use <strong>reflect-metadata</strong>:</p><ol><li><p>Install: <code>npm install reflect-metadata</code></p></li><li><p>Import once: <code>import 'reflect-metadata';</code></p></li><li><p>Enable <code>emitDecoratorMetadata</code> in <code>tsconfig.json</code>.</p></li></ol><pre><code><code>// Retrieve the type of a property
Reflect.getMetadata('design:type', target, propertyKey);
</code></code></pre><p>Reflection lets decorators inspect types, useful for form builders, DI, or validation.</p><div><hr></div><h2><strong>4. Angular Built-in Decorators</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zfYo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zfYo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 424w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 848w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 1272w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zfYo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png" width="601" height="386" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:386,&quot;width&quot;:601,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31034,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169727752?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zfYo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 424w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 848w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 1272w, https://substackcdn.com/image/fetch/$s_!zfYo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5706fc09-facc-4d1a-b8b4-9f3298252b10_601x386.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>Angular started using decorators <strong>long before</strong> TC39 finalized the spec &#8212; and relied on TypeScript&#8217;s experimental decorators. Although under the hood Angular relies on TypeScript&#8217;s standard compiler, it has its own <strong>Angular compiler (ngc)</strong> that processes the decorators and generates code for templates, DI, etc. See <a href="https://www.youtube.com/watch?v=FARnqSZaHMs">The Dance of Decorated Classes: Inside the Angular Compiler</a>. Hence, <code>ngc</code> can be said to be an extension TypeScript compiler which knows how to &#8220;execute&#8221; Angular decorators, applying their effects to the decorated classes at build time.</p></blockquote><div><hr></div><h2><strong>5. Writing Custom Decorators (New API)</strong></h2><h3><strong>5.1 Class Decorator</strong></h3><pre><code><code>function LogClass&lt;T extends new (...args: any[]) =&gt; any&gt;(constructor: T): T {
  console.log('Class created:', constructor.name);
  return constructor;
}

@LogClass
class MyService {}
</code></code></pre><h3><strong>5.2 Method Decorator</strong></h3><pre><code><code>function LogMethod&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      console.log(`Calling ${String(methodName)} with`, args);
      return originalMethod.call(this, ...args);
    };
  });
}
</code></code></pre><h3><strong>5.3 Field Decorator</strong></h3><pre><code><code>function DefaultValue&lt;T extends object, K extends keyof T&gt;(
  defaultValue: T[K]
) {
  return function (
    target: undefined, 
    context: ClassFieldDecoratorContext&lt;T, T[K]&gt;
  ): (value: T[K]) =&gt; T[K] {
    return (value: T[K]) =&gt; value ?? defaultValue;
  };
}

class Example {
  @DefaultValue("hello")
  message: string;
}
</code></code></pre><h3><strong>5.4 Accessor Decorator</strong></h3><pre><code><code>function Validate&lt;T extends object&gt;(
  target: undefined,
  context: ClassAccessorDecoratorContext&lt;T, any&gt;
): void {
  context.addInitializer(function (this: T) {
    const originalGet = this[context.name as keyof T] as any;
    const originalSet = context.access.set;
    
    context.access.set = function (value: any) {
      if (value === undefined || value === null) {
        throw new Error(`${String(context.name)} cannot be null or undefined`);
      }
      originalSet.call(this, value);
    };
  });
}
</code></code></pre><div><hr></div><h2><strong>6. Decorator Factories &amp; Composition</strong></h2><h3><strong>6.1 Decorator Factories</strong></h3><p>A <strong>decorator factory</strong> is a function that returns a decorator, allowing you to pass arguments/configuration.</p><pre><code><code>function LogWithPrefix(prefix: string) {
  return function &lt;T extends object&gt;(
    target: undefined,
    context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
  ): void {
    const methodName = context.name;
    context.addInitializer(function (this: T) {
      const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
      (this as any)[methodName] = function (this: T, ...args: any[]) {
        console.log(`${prefix}: ${String(methodName)}`, args);
        return originalMethod.call(this, ...args);
      };
    });
  };
}

class Example {
  @LogWithPrefix('DEBUG')
  say(message: string) {
    console.log(message);
  }
}
</code></code></pre><h3><strong>6.2 Decorator Composition</strong></h3><p><strong>Decorator composition</strong> means stacking multiple decorators on a single target. Decorators are applied <strong>bottom to top</strong> (the last decorator in code is applied first).</p><pre><code><code>function Log&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      console.log('Calling', String(methodName));
      return originalMethod.call(this, ...args);
    };
  });
}

function Cache&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  const cache = new Map&lt;string, any&gt;();
  
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      const cacheKey = JSON.stringify(args);
      if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
      }
      const result = originalMethod.call(this, ...args);
      cache.set(cacheKey, result);
      return result;
    };
  });
}

class MathService {
  @Log
  @Cache
  expensiveOperation(x: number) {
    return x * x; // pretend this is slow
  }
}
</code></code></pre><div><hr></div><h2><strong>7. Real-World Use Cases</strong></h2><h3><strong>7.1 Permission Check</strong></h3><pre><code><code>function RequirePermission(permission: string) {
  return function &lt;T extends object&gt;(
    target: undefined,
    context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
  ): void {
    const methodName = context.name;
    context.addInitializer(function (this: T) {
      const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
      (this as any)[methodName] = function (this: T, ...args: any[]) {
        const svc = inject(PermissionService);
        if (!svc.hasPermission(permission)) {
          throw new Error(`Missing ${permission}`);
        }
        return originalMethod.call(this, ...args);
      };
    });
  };
}
</code></code></pre><h3><strong>7.2 Caching</strong></h3><pre><code><code>const cache = new Map&lt;string, any&gt;();
function CacheResult&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      const ckey = `${String(methodName)}-${JSON.stringify(args)}`;
      if (cache.has(ckey)) return cache.get(ckey);
      const res = originalMethod.call(this, ...args);
      cache.set(ckey, res);
      return res;
    };
  });
}
</code></code></pre><h3><strong>7.3 Analytics on Component Init</strong></h3><pre><code><code>function WithAnalytics(eventName?: string) {
  return function &lt;T extends new (...args: any[]) =&gt; any&gt;(constructor: T): T {
    const originalInit = constructor.prototype.ngOnInit;
    constructor.prototype.ngOnInit = function () {
      const analytics = inject(AnalyticsService);
      analytics.trackEvent('Init', { name: eventName || constructor.name });
      originalInit?.apply(this);
    };
    return constructor;
  };
}
</code></code></pre><h3><strong>7.4 Auto-Unsubscribe</strong></h3><pre><code><code>function AutoUnsubscribe&lt;T extends new (...args: any[]) =&gt; any&gt;(constructor: T): T {
  const originalDestroy = constructor.prototype.ngOnDestroy;
  constructor.prototype.ngOnDestroy = function () {
    for (const k in this) {
      this[k]?.unsubscribe?.();
    }
    originalDestroy?.apply(this);
  };
  return constructor;
}
</code></code></pre><div><hr></div><h2><strong>8. Bonus: Simple Signals &amp; Observables</strong></h2><p>Bonus <strong><a href="https://newsletter.unstacked.dev/p/dev-101-custom-javascript-signals">Simple Signal</a></strong> inspired by Angular Signal and a custom <strong>fromObservable</strong> you can use to try out the @AutoUnsubscribe.</p><h3><strong>8.1 SimpleSignal</strong></h3><pre><code><code>function SimpleSignal&lt;T&gt;(initial: T) {
  let value = initial;
  const listeners: (() =&gt; void)[] = [];
  const signal = (() =&gt; value) as (() =&gt; T) &amp; {
    set: (v: T) =&gt; void;
    effect: (fn: () =&gt; void) =&gt; void;
  };
  signal.set = v =&gt; { value = v; listeners.forEach(fn =&gt; fn()); };
  signal.effect = fn =&gt; { listeners.push(fn); fn(); };
  return signal;
}
</code></code></pre><h3><strong>8.2 fromObservable</strong></h3><pre><code><code>import { Observable } from 'rxjs';
function fromObservable&lt;T&gt;(obs$: Observable&lt;T&gt;, initial: T) {
  const sig = SimpleSignal(initial);
  const sub = obs$.subscribe(val =&gt; sig.set(val));
  (sig as any).unsubscribe = () =&gt; sub.unsubscribe();
  return sig;
}
</code></code></pre><div><hr></div><h2><strong>9. Code Lab: Debounce Decorator</strong></h2><p><strong>Objective</strong>: Create a <code>@Debounce</code> method decorator that delays method execution.</p><pre><code><code>function Debounce(ms: number) {
  return function &lt;T extends object&gt;(
    target: undefined,
    context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
  ): void {
    const methodName = context.name;
    let timeoutId: NodeJS.Timeout;
    
    context.addInitializer(function (this: T) {
      const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
      (this as any)[methodName] = function (this: T, ...args: any[]) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() =&gt; {
          originalMethod.call(this, ...args);
        }, ms);
      };
    });
  };
}
</code></code></pre><p><strong>Usage:</strong></p><pre><code><code>class SearchComponent {
  @Debounce(300)
  onSearch(query: string) {
    // This will only execute after 300ms of no calls
  }
}
</code></code></pre><div><hr></div><h2><strong>10. Migration from Legacy API</strong></h2><h3><strong>Key Differences:</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pQrl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pQrl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 424w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 848w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 1272w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pQrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png" width="648" height="222" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ca51f08-d168-4639-91a5-42033de14974_648x222.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:222,&quot;width&quot;:648,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14238,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169727752?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pQrl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 424w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 848w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 1272w, https://substackcdn.com/image/fetch/$s_!pQrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca51f08-d168-4639-91a5-42033de14974_648x222.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3><strong>Migration Example:</strong></h3><p><strong>Legacy:</strong></p><pre><code><code>function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`, args);
    return original.apply(this, args);
  };
  return descriptor;
}
</code></code></pre><p><strong>New API:</strong></p><pre><code><code>function LogMethod&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      console.log(`Calling ${String(methodName)}`, args);
      return originalMethod.call(this, ...args);
    };
  });
}
</code></code></pre><div><hr></div><h2><strong>11. Best Practices &amp; Summary</strong></h2><ul><li><p><strong>Keep decorators small and single-purpose</strong></p></li><li><p><strong>Prefer decorator factories</strong> for configurability</p></li><li><p><strong>Document execution order</strong> if it matters</p></li><li><p><strong>Understand evaluation and application order</strong>: decorators are applied bottom-to-top</p></li><li><p><strong>Use reflection</strong> for advanced scenarios (type inspection, DI, validation)</p></li><li><p><strong>Leverage composition</strong> to separate concerns and reuse logic</p></li><li><p><strong>Use the new API</strong> for TypeScript 5.0+ projects</p></li><li><p><strong>Consider migration</strong> from legacy API for better performance and features</p></li></ul><div><hr></div><p>&#127881; You now have a complete advanced reference for TypeScript 5.0+ decorators, including real-world use cases, composition, and migration guidance.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Dev 101: TypeScript Legacy (Pre-v5.0) Decorators]]></title><description><![CDATA[The Decorator Pattern is a structural design pattern that attaches new behaviors to objects by placing them inside special wrapper objects, called decorators. This allows you to add functionality without modifying the original class.]]></description><link>https://newsletter.unstacked.dev/p/dev-101-typescript-legacy-pre-v50</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/dev-101-typescript-legacy-pre-v50</guid><dc:creator><![CDATA[Alex Muturi]]></dc:creator><pubDate>Fri, 01 Aug 2025 12:02:39 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3ee33890-b75a-4026-92c6-afeeb07637a0_612x408.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>1. Foundation: The Decorator Pattern</strong></h2><p>The <strong>Decorator Pattern</strong> is a structural design pattern that attaches new behaviors to objects by placing them inside special wrapper objects, called decorators. This allows you to add functionality without modifying the original class. To jump straight to the code: <a href="https://github.com/alex-migwi/angular-decorators-NG-Kenya-2025">[github repo]</a></p><pre><code><code>// Classical Decorator Pattern Example
class Coffee {
  cost() { return 5; }
}
class MilkDecorator {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1; }
}
const milkCoffee = new MilkDecorator(new Coffee());
console.log(milkCoffee.cost()); // 6
</code></code></pre><p>TypeScript legacy decorators are inspired by this concept: they let you <strong>add behavior</strong> or <strong>metadata</strong> to classes, methods, and properties in a declarative, compile-time manner.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2><strong>2. Pre-v5.0 Decorators: Basics &amp; Parameters</strong></h2><p>For TypeScript versions lesser than v5.0, decorators are supported using an <strong>experimental implementation</strong> based on an early version of a JavaScript proposal from the TC39 committee &#8212; the team that evolves the JavaScript language. To enable experimental support for decorators, you must enable the <code>experimentalDecorators</code> compiler option either on the command line or in your <code>tsconfig.json</code>.</p><blockquote><p>Find Typescript 5.0+ article: <a href="https://newsletter.unstacked.dev/p/dev-101-typescript-50-decorators">Typescript 5.0+ Decorators</a></p></blockquote><p>To use decorators in Pre-v5.0 TypeScript, enable them in <code>tsconfig.json</code>:</p><pre><code><code>{
  "target": "ES2017",
  "module": "commonjs",
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}</code></code></pre><p>Or using the command line:</p><pre><code>tsc --target ES5 --experimentalDecorators</code></pre><h3><strong>2.1 Types of Decorators &amp; Their Parameters</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b0xn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b0xn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 424w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 848w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 1272w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b0xn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png" width="1004" height="272" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:272,&quot;width&quot;:1004,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25903,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169724346?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!b0xn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 424w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 848w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 1272w, https://substackcdn.com/image/fetch/$s_!b0xn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8d78ef-b3ee-4f60-9a9b-647da2be7c03_1004x272.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4><strong>Parameter Details</strong></h4><ul><li><p><strong>Class Decorator</strong>: Receives the class constructor.</p></li><li><p><strong>Property Decorator</strong>: Receives the prototype and property name. Cannot modify descriptor directly.</p></li><li><p><strong>Method Decorator</strong>: Receives the prototype, method name, and descriptor (lets you override/wrap the method).</p></li><li><p><strong>Parameter Decorator</strong>: Receives the prototype, method name, and parameter index.</p></li></ul><pre><code><code>// Legacy API Examples
function MyClassDecorator(constructor: Function) {
  console.log('Class created:', constructor.name);
}

function MyPropertyDecorator(target: Object, propertyKey: string) {
  console.log('Property:', propertyKey);
}

function MyMethodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`, args);
    return original.apply(this, args);
  };
  return descriptor;
}

function MyParamDecorator(target: Object, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter ${parameterIndex} in ${propertyKey}`);
}
</code></code></pre><p><strong>Example Recap:</strong></p><pre><code><code>class Example {
  @MyPropertyDecorator
  someProperty: string;

  @MyMethodDecorator
  someMethod(@MyParamDecorator msg: string) {}
}
</code></code></pre><blockquote><p>Decorators execute at <strong>compile/design time</strong>. You can modify behavior or attach annotations used by frameworks.</p></blockquote><div><hr></div><h2><strong>3. Reflection &amp; Metadata</strong></h2><p>To read design-time types at runtime, use <strong>reflect-metadata</strong>:</p><ol><li><p>Install: <code>npm install reflect-metadata</code></p></li><li><p>Import once: <code>import 'reflect-metadata';</code></p></li><li><p>Enable <code>emitDecoratorMetadata</code> in <code>tsconfig.json</code>.</p></li></ol><pre><code><code>// Retrieve the type of a property
Reflect.getMetadata('design:type', target, propertyKey);
</code></code></pre><p>Reflection lets decorators inspect types, useful for form builders, DI, or validation.</p><div><hr></div><h2><strong>4. Angular Built-in Decorators</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rEh2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rEh2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 424w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 848w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 1272w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rEh2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png" width="601" height="386" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:386,&quot;width&quot;:601,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31034,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169724346?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rEh2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 424w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 848w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 1272w, https://substackcdn.com/image/fetch/$s_!rEh2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff57f2b70-0902-4a80-8c10-c48814f0ba71_601x386.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>Angular started using decorators <strong>long before</strong> TC39 finalized the spec &#8212; and relied on TypeScript&#8217;s experimental decorators. Although under the hood Angular relies on TypeScript&#8217;s standard compiler, it has its own <strong>Angular compiler (ngc)</strong> that processes the decorators and generates code for templates, DI, etc. See <a href="https://www.youtube.com/watch?v=FARnqSZaHMs">The Dance of Decorated Classes: Inside the Angular Compiler</a>. Hence, <code>ngc</code> can be said to be an extension TypeScript compiler which knows how to &#8220;execute&#8221; Angular decorators, applying their effects to the decorated classes at build time.</p></blockquote><div><hr></div><h2><strong>5. Writing Custom Decorators (Legacy API)</strong></h2><h3><strong>5.1 Class Decorator</strong></h3><pre><code><code>function LogClass(constructor: Function) {
  console.log('Class created:', constructor.name);
}

@LogClass
class MyService {}
</code></code></pre><h3><strong>5.2 Method Decorator</strong></h3><pre><code><code>function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return original.apply(this, args);
  };
  return descriptor;
}

class Example {
  @LogMethod
  say(message: string) {
    console.log(message);
  }
}
</code></code></pre><h3><strong>5.3 Property Decorator</strong></h3><pre><code><code>function DefaultValue(val: any) {
  return function (target: any, propertyKey: string) {
    let value = val;
    Object.defineProperty(target, propertyKey, {
      get: () =&gt; value,
      set: v =&gt; (value = v),
    });
  };
}

class Example {
  @DefaultValue("hello")
  message: string;
}
</code></code></pre><h3><strong>5.4 Parameter Decorator</strong></h3><pre><code><code>function MyParamDecorator(target: Object, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter ${parameterIndex} in method ${propertyKey}`);
}

class Example {
  someMethod(@MyParamDecorator msg: string) {}
}
</code></code></pre><div><hr></div><h2><strong>6. Decorator Factories &amp; Composition</strong></h2><h3><strong>6.1 Decorator Factories</strong></h3><p>A <strong>decorator factory</strong> is a function that returns a decorator, allowing you to pass arguments/configuration.</p><pre><code><code>function LogWithPrefix(prefix: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
    const original = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`${prefix}: ${propertyKey}`, args);
      return original.apply(this, args);
    };
    return descriptor;
  };
}

class Example {
  @LogWithPrefix('DEBUG')
  say(message: string) {
    console.log(message);
  }
}
</code></code></pre><h3><strong>6.2 Decorator Composition</strong></h3><p><strong>Decorator composition</strong> means stacking multiple decorators on a single target. Decorators are applied <strong>bottom to top</strong> (the last decorator in code is applied first).</p><pre><code><code>function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log('Calling', propertyKey);
    return original.apply(this, args);
  };
  return descriptor;
}

function Cache(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
  const cache = new Map&lt;string, any&gt;();
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const cacheKey = JSON.stringify(args);
    if (cache.has(cacheKey)) {
      return cache.get(cacheKey);
    }
    const result = original.apply(this, args);
    cache.set(cacheKey, result);
    return result;
  };
  return descriptor;
}

class MathService {
  @Log
  @Cache
  expensiveOperation(x: number) {
    return x * x; // pretend this is slow
  }
}
</code></code></pre><div><hr></div><h2><strong>7. Real-World Use Cases</strong></h2><h3><strong>7.1 Permission Check</strong></h3><pre><code><code>function RequirePermission(permission: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
    const original = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const svc = inject(PermissionService);
      if (!svc.hasPermission(permission)) {
        throw new Error(`Missing ${permission}`);
      }
      return original.apply(this, args);
    };
    return descriptor;
  };
}

class UserService {
  @RequirePermission('admin')
  deleteUser(id: string) {
    // Only admins can call this
  }
}
</code></code></pre><h3><strong>7.2 Caching</strong></h3><pre><code><code>const cache = new Map&lt;string, any&gt;();
function CacheResult(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const ckey = `${propertyKey}-${JSON.stringify(args)}`;
    if (cache.has(ckey)) return cache.get(ckey);
    const res = original.apply(this, args);
    cache.set(ckey, res);
    return res;
  };
  return descriptor;
}

class DataService {
  @CacheResult
  fetchUser(id: string) {
    // Expensive API call
  }
}
</code></code></pre><h3><strong>7.3 Analytics on Component Init</strong></h3><pre><code><code>function WithAnalytics(eventName?: string) {
  return function (constructor: any) {
    const originalInit = constructor.prototype.ngOnInit;
    constructor.prototype.ngOnInit = function () {
      const analytics = inject(AnalyticsService);
      analytics.trackEvent('Init', { name: eventName || constructor.name });
      originalInit?.apply(this);
    };
  };
}

@WithAnalytics('UserComponent')
class UserComponent {
  ngOnInit() {
    // Component initialization
  }
}
</code></code></pre><h3><strong>7.4 Auto-Unsubscribe</strong></h3><pre><code><code>function AutoUnsubscribe(constructor: any) {
  const originalDestroy = constructor.prototype.ngOnDestroy;
  constructor.prototype.ngOnDestroy = function () {
    for (const k in this) {
      this[k]?.unsubscribe?.();
    }
    originalDestroy?.apply(this);
  };
}

@AutoUnsubscribe
class MyComponent {
  private subscription = new Subject();
  
  ngOnDestroy() {
    // Will automatically unsubscribe all subscriptions
  }
}
</code></code></pre><div><hr></div><h2><strong>8. Bonus: Simple Signals &amp; Observables</strong></h2><p>Bonus Simple Signal with <strong>fromObservable </strong>custom implementation to plug into <strong>@AutoUnsubscribe</strong> decorator.</p><h3><strong>8.1 SimpleSignal</strong></h3><pre><code><code>function SimpleSignal&lt;T&gt;(initial: T) {
  let value = initial;
  const listeners: (() =&gt; void)[] = [];
  const signal = (() =&gt; value) as (() =&gt; T) &amp; {
    set: (v: T) =&gt; void;
    effect: (fn: () =&gt; void) =&gt; void;
  };
  signal.set = v =&gt; { value = v; listeners.forEach(fn =&gt; fn()); };
  signal.effect = fn =&gt; { listeners.push(fn); fn(); };
  return signal;
}
</code></code></pre><h3><strong>8.2 fromObservable</strong></h3><pre><code><code>import { Observable } from 'rxjs';
function fromObservable&lt;T&gt;(obs$: Observable&lt;T&gt;, initial: T) {
  const sig = SimpleSignal(initial);
  const sub = obs$.subscribe(val =&gt; sig.set(val));
  (sig as any).unsubscribe = () =&gt; sub.unsubscribe();
  return sig;
}
</code></code></pre><div><hr></div><h2><strong>9. Code Lab: Debounce Decorator</strong></h2><p><strong>Objective</strong>: Create a <code>@Debounce</code> method decorator that delays method execution.</p><pre><code><code>function Debounce(ms: number) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
        console.log("Inside the debounce decorator");
        const original = descriptor.value;
      let timeoutId: number;
      
      descriptor.value = function (...args: any[]) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() =&gt; {
          original.apply(this, args);
        }, ms);
      };
      
      return descriptor;
    };
  }

  class SearchComponent {
    @Debounce(300)
    onSearch(query: string) {
      // This will only execute after 300ms of no calls
      console.log('I waited for 300 ms to do nothing.... arghhhh!');
    }
  }
</code></code></pre><p><strong>Usage:</strong></p><pre><code><code>  // Test the debounce functionality
  const searchComponent = new SearchComponent();
  
  console.log('Calling onSearch multiple times quickly...');
  searchComponent.onSearch('test1');
  searchComponent.onSearch('test2');
  searchComponent.onSearch('test3');
  
  // The original method will only execute once after 300ms
  // because the debounce decorator delays execution and cancels previous calls
</code></code></pre><div><hr></div><h2><strong>10. Migration to New API</strong></h2><h3><strong>Key Differences:</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EWuZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EWuZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 424w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 848w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 1272w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EWuZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png" width="645" height="232" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:232,&quot;width&quot;:645,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14228,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/169724346?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EWuZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 424w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 848w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 1272w, https://substackcdn.com/image/fetch/$s_!EWuZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d354f23-4df5-488d-93aa-ec1f929c544b_645x232.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>Migration Example:</strong></p><p><strong>Legacy (Current):</strong></p><pre><code><code>function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`, args);
    return original.apply(this, args);
  };
  return descriptor;
}
</code></code></pre><p><strong>New API (Future):</strong></p><pre><code><code>function LogMethod&lt;T extends object&gt;(
  target: undefined,
  context: ClassMethodDecoratorContext&lt;T, (this: T, ...args: any[]) =&gt; any&gt;
): void {
  const methodName = context.name;
  context.addInitializer(function (this: T) {
    const originalMethod = this[methodName as keyof T] as (this: T, ...args: any[]) =&gt; any;
    (this as any)[methodName] = function (this: T, ...args: any[]) {
      console.log(`Calling ${String(methodName)}`, args);
      return originalMethod.call(this, ...args);
    };
  });
}
</code></code></pre><div><hr></div><h2><strong>11. Best Practices &amp; Summary</strong></h2><ul><li><p><strong>Keep decorators small and single-purpose</strong></p></li><li><p><strong>Prefer decorator factories</strong> for configurability</p></li><li><p><strong>Document execution order</strong> if it matters</p></li><li><p><strong>Understand evaluation and application order</strong>: decorators are applied bottom-to-top</p></li><li><p><strong>Use reflection</strong> for advanced scenarios (type inspection, DI, validation)</p></li><li><p><strong>Leverage composition</strong> to separate concerns and reuse logic</p></li><li><p><strong>Consider upgrading</strong> to new API for TypeScript 5.0+ projects</p></li><li><p><strong>Test thoroughly</strong> when migrating between APIs</p></li></ul><div><hr></div><p>&#127881; You now have a complete advanced reference for TypeScript legacy decorators, including real-world use cases, composition, and migration guidance to the new API. Review remarks are always welcome.</p><p>Happy decorating.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Building AI-powered e-commerce applications using Angular and Firebase AI Logic (formerly Vertex AI in Firebase)]]></title><description><![CDATA[A Practical Guide to Integrating Generative AI into Your Online Store with Angular and Firebase]]></description><link>https://newsletter.unstacked.dev/p/building-ai-powered-e-commerce-applications</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/building-ai-powered-e-commerce-applications</guid><dc:creator><![CDATA[Wayne Gakuo]]></dc:creator><pubDate>Wed, 18 Jun 2025 14:22:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rGAH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The landscape of online shopping has undergone a dramatic transformation. From the early days of simple digital catalogs, e-commerce has evolved into a dynamic, personalized, and highly competitive space. Today, the next frontier in this evolution is the integration of Artificial Intelligence (AI), which promises to make online retail more efficient and intuitive than ever before. AI is rapidly becoming a pivotal force in automating repetitive tasks, personalizing user experiences, and ultimately, driving sales and customer satisfaction.</p><p>This article will guide you through building a modern, AI-powered e-commerce application using <strong><a href="https://angular.dev/">Angular</a></strong> for the front end and leveraging the capabilities of <strong><a href="https://firebase.google.com/products/firebase-ai-logic">Google's Firebase AI Logic</a></strong> (formerly Vertex AI in Firebase). We will explore how to implement intelligent features by referencing code snippets from the "Bytewise" e-commerce application, a practical example I created, available on GitHub.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>The Inevitable Rise of AI in E-commerce</h2><p>Initially, e-commerce platforms focused on providing a convenient way to browse and purchase products. As the market matured, the focus shifted towards enhancing the user experience through features like customer reviews, wishlists, and basic recommendation engines. However, with the explosion of online stores and product variety, a new set of challenges emerged. Shoppers became overwhelmed with choices, and businesses struggled to differentiate themselves.</p><p>This is where AI steps in. By harnessing the power of machine learning and large language models (LLMs), e-commerce businesses can now:</p><ul><li><p><strong>Automate Customer Support:</strong> AI-powered chatbots can handle a vast range of customer inquiries 24/7, from order tracking to product questions, freeing up human agents to tackle more complex issues.</p></li><li><p><strong>Personalize Shopping Experiences:</strong> AI algorithms can analyze user behavior, purchase history, and even real-time interactions to provide highly personalized product recommendations, search results, and marketing content.</p></li><li><p><strong>Optimize Inventory and Pricing:</strong> Predictive analytics can forecast demand, helping businesses manage their stock levels more effectively and implement dynamic pricing strategies to maximize revenue.</p></li><li><p><strong>Streamline Product Discovery:</strong> AI can power intelligent search functionalities that understand natural language queries, making it easier for customers to find exactly what they are looking for.</p></li></ul><p></p><h2>Introducing Firebase AI Logic: The Brains of Your E-commerce App</h2><p>Firebase AI Logic is a powerful suite of tools that allows developers to easily integrate Google's cutting-edge generative AI models, like Gemini, directly into their web and mobile applications. It acts as a bridge between your application and powerful AI capabilities, handling the complexities of model deployment and scaling.</p><p>Firebase has evolved from "Vertex AI in Firebase" to the more robust "Firebase AI Logic," offering greater flexibility in building AI features.  Developers can now use either the free Gemini Developer API or the Vertex AI Gemini API, and importantly,  there's no need to include your Gemini API key directly in your app's code.</p><ul><li><p><strong>Gemini Developer API</strong>&#8212;billing is optional (available on the no-cost Spark plan)</p></li><li><p><strong>Vertex AI Gemini API</strong>&#8212;billing is required (requires the pay-as-you-go Blaze plan)</p></li></ul><p></p><h2>Building an AI-Powered E-commerce App with Angular and Firebase</h2><p>Let's dive into the practical aspects of building our AI-powered e-commerce application, "Bytewise." The application is built with Angular, a popular framework for creating dynamic single-page applications, and utilizes Firebase for its backend services, including Firebase AI Logic for its intelligent features.</p><p>You can find the complete source code for the Bytewise application on GitHub: <strong><a href="https://github.com/waynegakuo/bytewise">https://github.com/waynegakuo/bytewise</a></strong></p><p></p><h3>About Bytewise</h3><p>ByteWise is a fictional e-commerce platform that showcases electronic products with an integrated AI shopping assistant. The application features:</p><ul><li><p>Product catalog with detailed product information</p></li><li><p>AI assistant that can answer questions about products</p></li><li><p>Voice recognition for hands-free interaction with the AI assistant</p></li><li><p>Shopping cart functionality</p></li><li><p>Responsive UI with modern design</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rGAH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rGAH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 424w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 848w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rGAH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg" width="1280" height="558" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:558,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:136458,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/166131531?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rGAH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 424w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 848w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!rGAH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F190c1aa6-9ca6-49a8-94dc-f4d9616d7f95_1280x558.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Screenshot: ByteWise app in action</em></p><p></p><h3>Setting Up the Project<strong>&#128640;</strong></h3><p>This article assumes familiarity with Angular and Firebase setup. For setup instructions, please refer to the "<strong><a href="https://github.com/waynegakuo/bytewise?tab=readme-ov-file#setting-up-locally-prerequisites-">Setting Up Locally</a></strong>" section before <strong><a href="https://github.com/waynegakuo/bytewise?tab=readme-ov-file#getting-the-code-">cloning the project</a></strong>.</p><p>Alternatively, you can open the ByteWise project on <strong><a href="https://studio.firebase.google.com/import?url=https%3A%2F%2Fgithub.com%2Fwaynegakuo%2Fbytewise">Firebase Studio</a>, </strong>which will provide you with a web-based IDE with the requirements provisioned, and follow along.</p><p></p><h3>Firebase Project and Firebase AI Logic Setup<strong> &#128293;</strong></h3><p>Next, you'll need a Firebase project with a web application configured. You will also need to enable Firebase AI Logic on your Firebase project.</p><p>Follow the directions in <strong><a href="https://firebase.google.com/docs/vertex-ai/get-started?platform=web">Step 1 to create a project and a web app</a></strong>. You do not need to add any SDK code snippets during this step, as this example project already includes them.</p><p>After creating your Firebase project, go to the <strong><a href="https://console.firebase.google.com/project/_/ailogic?_gl=1*ehshqb*_ga*NTIzNzkxNTgzLjE3NDgzMjU2MzY.*_ga_CW55HF8NVT*czE3NTAyNTA5Mjkkbzg4JGcxJHQxNzUwMjUyNTYwJGo2MCRsMCRoMA..">Firebase AI Logic</a></strong> page in the Firebase console.  Click "Get Started" and select "Get started with this API" for the Gemini Developer API.  This API is preferable as it avoids the need to provide billing details.</p><p>Register your web app by clicking on the web icon as shown in the screenshot below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7qR3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7qR3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 424w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 848w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 1272w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7qR3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png" width="1376" height="562" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:562,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:184167,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/166131531?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7qR3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 424w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 848w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 1272w, https://substackcdn.com/image/fetch/$s_!7qR3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b53d22c-8d2c-4cc1-ab20-b61ee1fbc7ac_1376x562.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Leave the Firebase Hosting checkbox unchecked.  After obtaining your Firebase credentials (<code>firebaseConfig</code>), copy the entire contents; you'll need them in the next step.</p><p>Back in our codebase, in the <code>environment.ts</code> file, paste the contents of the Firebase configuration details into the <code>firebaseConfig</code> object. </p><pre><code><code>// src/environments/environment.ts

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: "&lt;your-api-key&gt;",
    authDomain: "&lt;your-app-domain&gt;",
    projectId: "&lt;your-project-id&gt;",
    storageBucket: "&lt;your-storage-bucket-id&gt;",
    messagingSenderId: "&lt;your-message-sender-id&gt;",
    appId: "&lt;your-app-id&gt;",
    measurementId: "&lt;your-measurement-id&gt;"
  },
};</code></code></pre><p>Install dependencies:</p><pre><code>npm install</code></pre><p></p><h3>Running the Application &#127939;&#8205;&#9794;&#65039;</h3><p>With the configuration complete, you can now start the development server:</p><pre><code>ng serve</code></pre><p>This command will build your Angular application and start a local development server. You can then access the application in your web browser at http://localhost:4200</p><p></p><h3>Core Architecture</h3><h4>Product State Management with Signals</h4><p>The foundation of our system uses Angular's signal-based state management:</p><pre><code>// src/app/services/product.service.ts

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private readonly products = signal&lt;Product[]&gt;([]);
  private readonly cart = signal&lt;Product[]&gt;([]);
  
  readonly cartCount = computed(() =&gt; this.cart().length);
  readonly totalPrice = computed(() =&gt; 
    this.cart().reduce((sum, item) =&gt; sum + item.price, 0)
  );

  constructor() {
    // Initialize with default products
    this.products.set([
      { id: 1, name: 'Laptop', price: 85000 },
      { id: 2, name: 'Smartphone', price: 35000 }
    ]);
  }

  getProducts() {
    return this.products();
  }

  addToCart(product: Product) {
    this.cart.update(current =&gt; [...current, product]);
  }

  clearCart() {
    this.cart.set([]);
  }
}</code></pre><p></p><h4>AI Service</h4><p>One of the most common and effective uses of AI in e-commerce is a customer support chatbot. In the Bytewise application, we have a <code>AiService </code>that handles the interaction with the Firebase AI Logic.</p><pre><code>// src/app/services/ai.service.ts

@Injectable({
  providedIn: 'root'
})
export class AiService {
  private readonly productService = inject(ProductService);
  private readonly model: GenerativeModel;
  private readonly chat: ChatSession;

  ...
}</code></pre><p>This service utilizes Angular's dependency injection system to maintain a clean, modular architecture. The service manages both the AI model and chat session, creating a seamless interface between user interactions and AI responses.</p><p></p><h3>AI Configuration and Tool Integration</h3><p>The system defines specific e-commerce operations through a structured tool set:</p><pre><code>// src/app/services/ai.service.ts

const productsToolSet: FunctionDeclarationsTool = {
  functionDeclarations: [
    {
      name: "getTotalNumberOfProducts",
      description: "Get the total number of products available in the store.",
    },
    {
      name: "getProducts",
      description: "Get an array of the products with the name and price of each product.",
    }
    // Additional function declarations...
  ]
};</code></pre><p>This tool set enables the AI to perform specific actions within the e-commerce system, from checking inventory to managing shopping carts.</p><p></p><h3>Strong Type Safety with Schema Definitions</h3><p>Product data integrity is maintained through robust schema definitions:</p><pre><code><code>// src/app/services/ai.service.ts

...

Schema.object({
  properties: {
    productsToAdd: Schema.array({
      items: Schema.object({
        properties: {
          name: Schema.string({
            description: "The name of the product.",
          }),
          price: Schema.number({
            description: "The numerical price of the product.",
          }),
        },
        required: ["name", "price"],
      }),
    }),
  },
})

...</code></code></pre><p>This schema ensures that all product-related operations maintain data consistency and type safety.</p><p></p><h3>Gemini Developer API Setup and System Instructions</h3><p>The integration with Google's Gemini AI is configured with specific business rules:</p><pre><code>// src/app/services/ai.service.ts

...

const geminiAI = getAI(this.firebaseApp, {backend: new GoogleAIBackend()});
const systemInstruction =
  "Welcome to ByteWise. You are a superstar agent for this ecommerce store..." ;

this.model = getGenerativeModel(geminiAI , {
  model: "gemini-2.0-flash",
  systemInstruction: systemInstruction,
  tools: [productsToolSet],
});

...</code></pre><p>The code initializes a Gemini AI-powered chatbot for the e-commerce store, which will interact with the inventory system and shopping cart, and is equipped with tools to perform various shopping-related functions. </p><p>The <code>systemInstruction</code> property is crucial as it helps the AI model understand its context and maintain consistent formatting when interacting with users about products and prices in the ByteWise ecommerce store.</p><h3>AI Interaction Processing</h3><p>The system handles AI interactions through a sophisticated message processing system:</p><pre><code>// src/app/services/ai.service.ts

...

async askAgent(userPrompt: string) {
  let result = await this.chat.sendMessage(userPrompt);
  const functionCalls = result.response.functionCalls();

  if(functionCalls &amp;&amp; functionCalls.length &gt; 0) {
    for (const functionCall of functionCalls) {
      switch (functionCall.name) {
        case "getTotalNumberOfProducts": {
          const functionResult = this.getTotalNumberOfProducts();
          // Process response
          break;
        }
        // Additional case handling...
      }
    }
  }
  return result.response.text();
}

...</code></pre><p>The <code>askAgent </code>function takes a user's prompt (their question or message) as input, sends it to the Gemini model via Firebase AI Logic, and returns the model's generated text response.</p><p>Think of it as a smart assistant for an e-commerce store that can understand user requests and perform actions. Here's how it works:</p><ol><li><p><strong>Taking User Input</strong>:</p></li></ol><ul><li><p>When a user types something (like "How many products do you have?" or "Add this to my cart"), the method takes this message as a <code>userPrompt</code></p></li><li><p>It sends this message to an AI chat system</p></li></ul><ol start="2"><li><p><strong>Checking for Actions</strong>:</p></li></ol><ul><li><p>The AI analyzes the message and decides if it needs to perform any specific actions (called "function calls")</p></li><li><p>For example, if you ask "How many products are there?", it will need to check the actual product count</p></li></ul><ol start="3"><li><p><strong>Handling Different Actions</strong>: The method can handle four main types of actions:</p></li></ol><ul><li><p><code>getTotalNumberOfProducts</code>: Counts all products in the store</p></li><li><p><code>getProducts</code>: Gets the list of all available products</p></li><li><p><code>clearCart</code>: Empties the shopping cart</p></li><li><p><code>addToCart</code>: Adds specific products to the cart</p></li></ul><ol start="4"><li><p><strong>Completing the Conversation</strong>:</p></li></ol><ul><li><p>After performing any necessary actions, it sends the results back to the AI</p></li><li><p>The AI then forms a natural response to the user</p></li><li><p>This response is returned to be shown to the user</p></li></ul><p>It's like having a helpful store assistant who can both answer questions and perform actions for you!</p><p></p><h3>Service Integration in Components</h3><p>Bringing it all together in the user interface:</p><pre><code>// src/app/components/agent-window.component.ts


@Component({
  selector: 'app-agent-window',
  ...
})
export class AgentWindowComponent {
  private readonly aiService = inject(AiService);
  readonly productService = inject(ProductService);
  ....

  constructor() {
    this.productList.set(this.productService.getProducts());
    ...
  }

  async sendMessage(): Promise&lt;void&gt; {
    if (this.userInput.trim() === '') return;

    // Add user message
    this.messageHistory.update((history) =&gt;
      [...history, { text: this.userInput, isUser: true }]
    );

    // Clear input
    const userQuestion = this.userInput;
    this.userInput = '';

   ...

    try {
      const response = await this.aiService.askAgent(userQuestion);
      this.messageHistory.update((history) =&gt; [
        ...history,
        { isUser: false, text: response }
      ]);
    } finally {
      // Set thinking state to false regardless of success or failure
      this.isThinking.set(false);
    }
  }

  ...
}</code></pre><p>This component serves as the main interface for user-AI interaction in your e-commerce platform, providing a clean and intuitive chat experience while maintaining a reactive and performant implementation using Angular&#8217;s signals.</p><div><hr></div><h2>Conclusion</h2><p>The integration of AI is no longer a futuristic concept in e-commerce; it's a present-day necessity for creating engaging, efficient, and personalized online shopping experiences. By combining the power of a versatile front-end framework like Angular with the accessible and powerful AI capabilities of Firebase AI Logic, developers can build the next generation of e-commerce applications.</p><p>The Bytewise example provides a solid foundation for understanding the practical implementation of these concepts. As AI models continue to evolve and become more sophisticated, the possibilities for innovation in e-commerce are boundless. The journey to a truly intelligent online store starts with embracing these powerful tools and reimagining what's possible in the world of online retail.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Genkit Tool Calling: Give AI Models (LLMs) the Tools to Get Things Done]]></title><description><![CDATA[A practical walkthrough of Genkits' tool-calling, demonstrating how to connect AI Models to your internal systems to get things done through natural language]]></description><link>https://newsletter.unstacked.dev/p/genkit-tool-calling-give-your-ai</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/genkit-tool-calling-give-your-ai</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Mon, 16 Jun 2025 03:51:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/57eb70f7-d1b3-40e4-b2cb-9a80bab49f58_1280x853.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI Models are taking over the world, and we are all looking for ways to use them to solve different problems. One common issue with <strong>Large Language Models</strong> (LLMs) is their training data, in that they only know what they were trained on. This is great when we want them to answer general questions, but not so useful when we want them to answer questions based on private data, i.e., customer data.</p><p>There are a number of ways to solve this issue other than training your own AI models. One of the options is <strong>Retrieval-Augmented Generation</strong> <strong>(RAG)</strong>, where we ground the LLMs based on external data to improve the accuracy and relevance of the responses.</p><p>The other option is <strong>Function Calling</strong>, which is the subject of today&#8217;s article. <strong>Function calling</strong>&#8212;known as <strong>tool calling</strong> in Genkit&#8212;involves giving AI Models a structured way to interact with your application/system or external data sources (APIs) to accomplish various tasks and ensuring it has up-to-date data. The AI Model will then call those functions we provide to accomplish various tasks, such as to fetch user data, create a ticket for the user, and so on.</p><p>To demonstrate this, we will be building a simple customer support agent that will answer questions from customers, and the AI Model will use the tools we provide to determine which tools it needs to call to accomplish the customer&#8217;s goal.</p><div class="pullquote"><p>If you are new to Genkit, please check out the official <a href="https://genkit.dev/docs/get-started/">docs</a> on how to get started, as this is beyond the scope of this post.</p><p><strong>Please subscribe for future posts like this:</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/subscribe?"><span>Subscribe now</span></a></p></div><h2>Defining Genkit Tools</h2><p>In Genkit, tools are special functions that contain information about what they are meant to accomplish and the means to accomplish the intended task &#8212; a function. </p><p>A tool in Genkit is defined using the <code>defineTool</code> method which accepts two arguments: a config object and a function.</p><pre><code>ai.defineTool(
  {
    name: 'toolName',
    description: 'Tool Description',
  },
  async ({ customerId }) =&gt; {
    // implementation to fetch customer details
  },
);</code></pre><blockquote><p><strong>Please Note:</strong></p><p>The <code>ai</code> instance is created by calling genkit function, passing in a number of configurations such as plugins for the AI Model you want to use, and the default model. For more information, checkout the Genkit documentation, <a href="https://genkit.dev/">here</a>.</p><pre><code>import { gemini20Flash, googleAI } from '@genkit-ai/googleai';
import { genkit } from 'genkit';

const ai = genkit({
  plugins: [googleAI()],
  model: gemini20Flash,
});</code></pre></blockquote><div class="pullquote"><p><strong>IMPORTANT!</strong> </p><p>Genkit tool calling support depends on the model, the model API, and the Genkit plugin for the model. Not all models support tool calling. Please consult the documentation to ensure that the model you intend to use supports Genkit tool calling.</p><p>For this post, we will be using <strong>Gemini 2.0 Flash</strong>, which supports tool calling.</p></div><p>A config is where we describe what the tool does; we give a descriptive name and a description that describes the tool's intended purpose. We can also include input and output JSON schemas (supports <a href="https://zod.dev/">zod</a> out of the box) among other options. Only the description and name are required.</p><p>The second argument is the function where we implement the tool&#8217;s objective. For instance, inside the function, we can make a DB call, an API call or any other deterministic operation, based on the tool&#8217;s intended objective.</p><p>For instance, in our case, we might want to define a tool to fetch customer information, given a customer ID. In this case, we would define a <code>GetCustomerDetails</code> tool that looks up the customer details from our database. </p><p>Please note we are providing an input and output schema to ensure data is returned in a very specific format, all the time, and we will ensure we provide a good description of our tool.</p><pre><code>const getCustomerDetailsTool = ai.defineTool(
  {
    name: 'GetCustomerDetails',
    description: 'Fetches details of a customer by ID',
    inputSchema: z.object({
      customerId: z.string().describe('The ID of the customer to fetch details for'),
    }),
    outputSchema: customerSchema,
  },
  async ({ customerId }) =&gt; {
    // This could be a database call or an API request in a real application.
    const customer = customers.find((customer) =&gt; customer.id === customerId);
    if (!customer) {
      throw new Error(`Customer with ID ${customerId} not found`);
    }
    return customer;
  },
);</code></pre><p>Now that we have created our first tool, we will need to pass it to the AI Model so it can use it, if need be, based on the customer&#8217;s question. In Genkit, there are several ways of achieving this, depending on whether we are using a flow, a prompt, or we are invoking the generate method directly inside the function.</p><p>When calling the <code>generate</code> method, passing the tool, in the tools&#8217; options, alongside the prompt as shown below:</p><pre><code>const { text } = await ai.generate({
  prompt: `
  You are an eCommerce Customer Service AI.
  You can answer questions about customers and their orders.
  You have access to the following tools:
  - Get Customer Details: Fetches details of a customer by ID.
  - Get Customer Orders: Fetches orders for a specific customer, by customer ID.
  Use the tools to answer questions about customers and their orders.
  If you need to fetch customer details, use the Get Customer Details tool.
  If you need to fetch customer orders, use the Get Customer Orders tool.
  If you cannot answer a question, respond with "I don't know" or "I cannot answer that question".

  When answering questions, use the following format:
  - If the question is about customer details, provide the customer's name, email, and phone number.
  - If the question is about customer orders, provide the number of orders placed and details of the most recent order.
  - If the question is about a product, provide the product name, price, and description.

  Use all tools to fetch the necessary information, do not return IDs, instead return the relevant details the user is interested in.

  Example questions:
  - What is the email address of customer with ID 1?
  - How many orders has customer with ID 2 placed?
  
  The customer id is ${customerID} and the question is: ${question}
  `,
  tools: [getCustomerDetailsTool],
  toolChoice: 'auto',
});</code></pre><blockquote><p>As you can see, our prompt is rather detailed, as we have to clearly instruct the AI Model to behave exactly the way we want, and reduce instances of it hallucinating. In cases where it has no answer, we want the AI model to say it doesn&#8217;t know, instead of hallucinating.</p></blockquote><p>We can put the above <code>generate</code> method call in a function, and call it as shown below. On top of that, we are passing our tool &#8212; <code>getCustomerDetailsTool</code> &#8212; in the tools section.</p><pre><code>async function run() {
  const { text } = await ai.generate(...);
  // log our response
  console.log(text);
}

run();</code></pre><p>However, running the above can be very difficult, as you need to pass in the inputs in the correct format and almost impossible to debug. This is where <strong>Genkit Developer UI</strong> comes in.</p><h3>Genkit Developer UI</h3><p>Genkit provides a developer UI where you can view, invoke and test your flows, prompts, tools, among others, in a very user-friendly UI, which enhances the developer experience.</p><blockquote><p>To launch the Genkit Developer UI, you can do so by running the following command:</p><pre><code>npx genkit start -- npx tsx --watch index.ts</code></pre><p>In the above command, genkit is the genkit cli, and <a href="https://tsx.is/getting-started">tsx</a> is an NPM package for executing TypeScript files, without needing to transpile to Javascript first. <code>index.ts</code> is the file which contains our code, and the entry file of our project.</p></blockquote><p>Since we have already defined our tool, running the above command, we should be able to see it in the UI and even interact with it, as shown below:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;3f3d804e-ea1a-4f55-ba31-de5788f68042&quot;,&quot;duration&quot;:null}"></div><h4>Defining a Genkit Flow</h4><p>For our case, we are going to switch to a flow. We will define a flow using the <code>defineFlow</code> method, which takes two arguments &#8212; a config object and a function to be executed when the flow is called.</p><p>For the flow config object, we will set the name of the flow, the input and output schema, keeping things simple for now. In the function, we will call the generate method, just like we previously did. We will return the text, so we can see the response of the AI Model in the UI.</p><pre><code>const assistant = ai.defineFlow(
  {
    name: 'eCommerceCustomerServiceAI',
    inputSchema: z.object({
      customerID: z.string().describe('The ID of the customer to fetch details for'),
      prompt: z.string().describe('The question or request for customer service'),
    }),
    outputSchema: z.string().describe('The response from the AI assistant'),
  },
  async ({ customerID, prompt }) =&gt; {
    const { text } = await ai.generate({
      prompt: `
      You are an eCommerce Customer Service AI.
      You can answer questions about customers and their orders.
      You have access to the following tools:
      - Get Customer Details: Fetches details of a customer by ID.
      - Get Customer Orders: Fetches orders for a specific customer, by customer ID.
      Use the tools to answer questions about customers and their orders.
      If you need to fetch customer details, use the Get Customer Details tool.
      If you need to fetch customer orders, use the Get Customer Orders tool.
      If you cannot answer a question, respond with "I don't know" or "I cannot answer that question".

      When answering questions, use the following format:
      - If the question is about customer details, provide the customer's name, email, and phone number.
      - If the question is about customer orders, provide the number of orders placed and details of the most recent order.
      - If the question is about a product, provide the product name, price, and description.

      Use all tools to fetch the necessary information, do not return IDs, instead return the relevant details the user is interested in.
  
      Example questions:
      - What is the email address of customer with ID 1?
      - How many orders has customer with ID 2 placed?
      
      The customer id is ${customerID} and the question is: ${prompt}
      `,
      tools: [getCustomerDetailsTool],
      toolChoice: 'auto',
    });

    return text;
  },
);</code></pre><p>Our input schema will accept a customer ID and a question from the customer. The customer ID would likely come from the user session, while the question would be what the prompt the user entered in the chat input or system-generated, doesn't matter that much for this article.</p><p>Next, we can use the developer UI to test the flow and see if we can get some user details from our database (I know it&#8217;s fake).</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;58592f70-e5f3-4f0f-a921-f86d224fb0ca&quot;,&quot;duration&quot;:null}"></div><p>You may be wondering, how do we know it&#8217;s working? The developer UI provides a stack trace to show you what exactly when during the execution, as shown below:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;49bae39e-54f9-44f3-8943-2d1dde38e35f&quot;,&quot;duration&quot;:null}"></div><p>As you can see, this can be very helpful when troubleshooting your Genkit application and trying to figure out where things are going wrong. It will show you all the calls it made to the tools, including the inputs and outputs of each step.</p><h3>Finalising the Agent</h3><p>As it currently stands, no customer will be asking about their names, but hopefully, you get the idea. The customer would likely want to know the status of their orders, among other things. We will add more tools for getting order information and product information, following the template we have seen earlier, as shown below:</p><pre><code>const getCustomerOrdersTool = ai.defineTool(
  {
    name: 'GetCustomerOrders',
    description: 'Fetches orders for a specific customer, by customer ID',
    inputSchema: z.object({
      customerId: z.string().describe('The ID of the customer to fetch orders for'),
    }),
    outputSchema: z.array(orderSchema),
  },
  async ({ customerId }) =&gt; {
    return orders.filter((order) =&gt; order.customerId === customerId);
  },
);

const getProductDetailsTool = ai.defineTool(
  {
    name: 'GetProductDetails',
    description: 'Fetches product details by product ID',
    inputSchema: z.object({
      productId: z.string().describe('The ID of the product to fetch details for'),
    }),
    outputSchema: productSchema,
  },
  async ({ productId }) =&gt; {
    const product = products.find((product) =&gt; product.id === productId);
    if (!product) {
      throw new Error(`Product with ID ${productId} not found`);
    }
    return product;
  },
);</code></pre><p>And then, we will add the above tools to the tools section, in the generate method, next to the one we already have, as shown below:</p><pre><code>const assistant = ai.defineFlow(
  {
    // ... nothing changes here
  },
  async ({ customerID, prompt }) =&gt; {
    const { text } = await ai.generate({
      prompt: `...`,
      tools: [
        getCustomerDetailsTool, 
        // we add the tools here
        getCustomerOrdersTool, 
        getProductDetailsTool
      ],
    });

    return text;
  },
);</code></pre><p>And finally, we can use the Genkit Developer UI to test whether our flow and tools work.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;7af373f9-31cd-420d-8a7e-a2b8c03f55ba&quot;,&quot;duration&quot;:null}"></div><p>As you can see, the AI model &#8212; in our case, Gemini &#8212; figures out the status of the customers&#8217; orders and associates them with products by utilising the different tools we provided. While the prompt can be improved to allow the customer not to be very specific, and still accomplish the same goals, hopefully, you get the idea of how we can give LLMs the ability to interact with our systems to accomplish different goals.</p><p>If you are interested, you can find the above source code <a href="https://github.com/mainawycliffe/genkit-tool-calling">here</a>.</p><h2>Conclussion</h2><p>In this post, we discussed <strong>function calling</strong> that allows you to give AI models access to your applications and system, so that they can get things done. We also looked at the Genkit Developer UI, for the purpose of testing and debugging our flows, tools and applications we are building on top of AI models.</p><p>I hope this article gave you ideas on how you can use Genkit to build AI agents to help accomplish various tasks on behalf of customers, and if you have any questions, please feel free to put them in the comment section below.</p><p>Until next time, keep on learning.</p><div><hr></div><h2><strong>Ready to Move From AI Hype to Real-World Results?</strong></h2><p>At Unstacked Labs, we specialise in building practical AI agents, like the one in this article, that automate complex workflows and solve real business problems.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://unstacked.dev/&quot;,&quot;text&quot;:&quot;Let's discuss your company needs&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://unstacked.dev/"><span>Let's discuss your company needs</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Dev 101: Custom JavaScript Signals Inspired by Angular’s Signals]]></title><description><![CDATA[Interestingly, Angular Signals work like a function and have an object property as a function. What if you could build your own reactive signal system... in plain JavaScript? Inspired by Angular Signals, this fun experiment shows how to create callable custom signals without any frameworks &#8212; just functions, reactivity, and a bit of magic &#129668;. Give your JS projects superpowers &#128165;]]></description><link>https://newsletter.unstacked.dev/p/dev-101-custom-javascript-signals</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/dev-101-custom-javascript-signals</guid><dc:creator><![CDATA[Alex Muturi]]></dc:creator><pubDate>Mon, 05 May 2025 13:56:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JYC2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my quest to understand how Signals work, I, read Angular Docs and tutes, read tutorials, watched videos, listened to talks etc but still I wanted to understand the magic behind this interesting syntax that allowed me to call an Angular Signal like a function and still invoke an object property as a function.</p><pre><code>// Angular Signals

const count = signal(0);
// then call the Signal like a function - reads the value.
console.log('The count is: ' + count());

// invoke Object function
count.set(3);</code></pre><p>And that is when i came across the idea Javascript Callable Functions and also reminded me of the fact that in JavaScript all functions are object methods.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Our exercise works based on the premise that a callable object behaves as both as an object and a function. You can access it and assign its properties values <code>obj.bar = value</code>, call its methods <code>obj.foo()</code>, but also call the object directly <code>obj()</code>, as if it were a function.</p><p>In order for all these things to make sense we also refer to the classical constructor pattern used in <code>object-oriented programming languages</code>. The <code>constructor pattern</code> is a style used to initialise a newly created object once memory has been allocated for it. In our case here, we&#8217;re interested in <em>object</em> constructors.</p><p>Object constructors will create specific types of objects. The process of creating these objects can accept arguments through a constructor to set the values of member properties and methods when the object is first created. Once the object is ready we can manipulate properties via setters, access them via getters and call object methods.</p><p>JavaScript doesn&#8217;t support the concept of classes, but it does support special constructor functions that work with objects. By simply prefixing a call to a constructor function with the keyword <code>new</code>, we can tell JavaScript we would like the function to behave like a constructor and instantiate a new object with the members defined by that function. Inside a constructor, the keyword <code>this</code> references the new object that&#8217;s being created.</p><p>I will assume that going on forward you have a good understanding of the constructor pattern (plus you have taken some time to read about it if this is new) as this will form a foundational concept for the idea behind the custom simple signal we are about to build.</p><blockquote><p>The idea we are pursuing here does not imply this is how Angular Signals code looks like, but rather it is exploring an idea in an attempt to understand possible patterns and how Angular Signals concept works.</p></blockquote><p>Alright, lets ride this wave&#8230;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JYC2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JYC2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 424w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 848w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JYC2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JYC2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 424w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 848w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!JYC2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d3c53a-f2eb-47bc-92ef-0c9d6366a49d_1600x1067.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@arstyy?utm_source=medium&amp;utm_medium=referral">Austin Neill</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure></div><p>First we start at the very basic, how do achieve this in typescript&#8230;</p><pre><code>obj = new ObjClass(); 
this.obj() // this call runs a certain function</code></pre><p>In TypeScript, to achieve this behavior where calling <code>this.obj()</code> runs a certain function, you can make use of a <strong>getter</strong> or explicitly define a method in your class.</p><p>Here&#8217;s how you can do it:</p><h3>1. Using a Getter</h3><p>You can define a getter in your class that executes a specific function when accessed:</p><pre><code>class ObjClass {
  private someMethod(): void {
    console.log("Function is called!");
  }

  get obj(): () =&gt; void {
    return this.someMethod.bind(this);
  }
}

const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"</code></pre><h3>2. Defining a Method</h3><p>Alternatively, you can directly define a method in the class instead of using a getter.</p><pre><code>class ObjClass {
  obj(): void {
    console.log("Function is called!");
  }
}

const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"</code></pre><h3>3. Using a Property with an Arrow Function</h3><p>If you want more control or don&#8217;t need to bind <code>this</code> manually, you can assign an arrow function to a property:</p><pre><code>class ObjClass {
  obj = () =&gt; {
    console.log("Function is called!");
  };
}

const instance = new ObjClass();
instance.obj(); // This will log "Function is called!"</code></pre><h3>Key Differences</h3><ul><li><p><strong>Getter:</strong> Method to be accessed as a property but still invoke a function.</p></li><li><p><strong>Method:</strong> Conventional way of defining functions in a class.</p></li><li><p><strong>Arrow Function Property:</strong> Ensure <code>this</code> is always bound to the class instance.</p></li></ul><p>Lets move a notch higher, to how do we call the instance of a class like a function.</p><pre><code>class ObjClass {
  obj = () =&gt; {
    console.log("Function is called!");
  };
}

const instance = new ObjClass();
instance(); // This will log "Function is called!"</code></pre><h3>Using a Function Constructor</h3><p>You can define the class in a way that the constructor returns a callable function.</p><pre><code>class ObjClass {
  obj = () =&gt; {
    console.log("Function is called!");
  };

  constructor() {
    const callable = () =&gt; this.obj();
    return Object.assign(callable, this); // Combine the function and the class instance
  }
}

const instance = new ObjClass() as unknown as () =&gt; void;
instance(); // Logs "Function is called!"</code></pre><p>The constructor creates a callable function (<code>callable</code>) that calls the <code>obj</code> method. <code>Object.assign</code> merges the class properties and methods into the function, so <code>instance</code> behaves like both a function and an instance of the class.</p><h3>Angular Signals</h3><p>Angular signals were introduced as part of Angular&#8217;s reactive system, starting with Angular 16. Signals provide a <strong>reactive primitive</strong> for managing and tracking state changes, similar to reactive systems in frameworks like <strong>SolidJS</strong>. They allow you to manage data reactivity without relying heavily on observables or subscriptions.</p><h3>Key Features of Angular Signals</h3><ol><li><p><strong>Declarative Reactivity</strong>:<br>Signals automatically track dependencies and update the UI when values change.</p></li><li><p><strong>Synchronous and Predictable</strong>:<br>Unlike observables, signals are synchronous and provide direct access to the current value without requiring <code>.subscribe()</code>.</p></li><li><p><strong>Track Dependencies Automatically</strong>:<br>Signals automatically capture dependencies during execution, which simplifies state management.</p></li><li><p><strong>Composable</strong>:<br>You can create derived signals and combine signals to manage more complex state transformations.</p></li></ol><p>In Angular a signal can created like so:</p><pre><code>import { signal } from '@angular/core';

const count = signal(0);

console.log(count()); // Get the current value: 0
count.set(5);         // Update the value
console.log(count()); // Output: 5</code></pre><p>For more on signals: <a href="https://angular.dev/guide/signals">Angular Signal Docs</a></p><h3>Minimal Custom Signal Implementation using a Class</h3><p>Lets create an <strong>overly simplified version of Angular signals</strong> using just a class in TypeScript. The idea is to have reactive primitives that can store a value, update it, and notify listeners (like effects).</p><pre><code>class SimpleSignal&lt;T&gt; {
  private value: T;
  private listeners: (() =&gt; void)[] = [];

  constructor(initialValue: T) {
    this.value = initialValue;
  }

  // Getter for the current value
  get(): T {
    return this.value;
  }

  // Setter for the value, notifying all listeners
  set(newValue: T): void {
    this.value = newValue;
    this.notify();
  }

  // Subscribe to changes
  effect(listener: () =&gt; void): void {
    this.listeners.push(listener);
    // Run the effect once initially
    listener();
  }

  // Notify all listeners of the value change
  private notify(): void {
    for (const listener of this.listeners) {
      listener();
    }
  }
}</code></pre><p>Usage Examples:</p><ol><li><p>Create SimpleSignal</p></li></ol><pre><code>const count = new SimpleSignal(0);

// Access the current value
console.log(count.get()); // Output: 0</code></pre><p>2. Update SimpleSignal</p><pre><code>count.set(5);
console.log(count.get()); // Output: 5</code></pre><p>3. Create an Effect</p><pre><code>count.effect(() =&gt; {
  console.log(`Count changed to: ${count.get()}`);
});

count.set(10); // Logs: "Count changed to: 10"
count.set(20); // Logs: "Count changed to: 20"</code></pre><h3>Minimal Custom Computed Signal Implementation using a Class</h3><pre><code>class SimpleComputedSignal&lt;T&gt; {
  private computeFn: () =&gt; T;
  private listeners: (() =&gt; void)[] = [];
  private cachedValue!: T;

  constructor(computeFn: () =&gt; T) {
    this.computeFn = computeFn;
    this.cachedValue = this.computeFn();
  }

  get(): T {
    return this.cachedValue;
  }

  update(): void {
    this.cachedValue = this.computeFn();
    this.notify();
  }

  effect(listener: () =&gt; void): void {
    this.listeners.push(listener);
    listener();
  }

  private notify(): void {
    for (const listener of this.listeners) {
      listener();
    }
  }
}

// Usage:
const baseCount = new SimpleSignal(2);
const doubleCount = new SimpleComputedSignal(() =&gt; baseCount.get() * 2);

doubleCount.effect(() =&gt; {
  console.log(`Double Count: ${doubleCount.get()}`);
});

baseCount.set(3); // Logs: "Double Count: 6"</code></pre><h3>Minimal Custom Signal Implementation using a Function</h3><p>Now that we&#8217;ve seen how we can do this using a class, let&#8217;s see how we can this using function&#8230; hint: <code>remember that functions are objects in Javascript</code>.</p><pre><code>function SimpleSignal&lt;T&gt;(initialValue: T) {
  let value = initialValue;
  const listeners: (() =&gt; void)[] = [];

  // The callable signal function
  const signal = (() =&gt; value) as (() =&gt; T) &amp; {
    set: (newValue: T) =&gt; void;
    effect: (listener: () =&gt; void) =&gt; void;
  };

  // Add the `set` method to update the value and notify listeners
  signal.set = (newValue: T) =&gt; {
    value = newValue;
    listeners.forEach((listener) =&gt; listener());
  };

  // Add the `effect` method to register listeners
  signal.effect = (listener: () =&gt; void) =&gt; {
    listeners.push(listener);
    // Run the effect immediately
    listener();
  };

  return signal;
}</code></pre><p>Usage:</p><pre><code>// create simple signal
const baseCount = SimpleSignal(0);

// get the value
console.log(baseCount()); // Output: 0

// update value
baseCount.set(5);
console.log(baseCount()); // Output: 5

// add an effect
baseCount.effect(() =&gt; {
  console.log(`Base count changed to: ${baseCount()}`);
});

baseCount.set(10); // Logs: "Base count changed to: 10"
baseCount.set(20); // Logs: "Base count changed to: 20"</code></pre><p>So what have we here:<br><code>SimpleSignal</code><strong> Function</strong>:</p><ol><li><p>Returns a callable function (<code>signal</code>) that holds the current value.</p></li><li><p>The <code>signal</code> function is enhanced with additional properties: <code>set</code> and <code>effect</code>.</p></li></ol><p><code>signal()</code>:</p><ol><li><p>Acts as a getter for the signal value.</p></li><li><p>When invoked, it simply returns the current value.</p></li></ol><p><code>signal.set(newValue)</code>:</p><ol><li><p>Updates the signal&#8217;s value and notifies all registered listeners.</p></li></ol><p><code>signal.effect(listener)</code>:</p><ol><li><p>Registers a listener that gets called whenever the signal value changes.</p></li></ol><h3>Minimal Custom Computed Signal Implementation using a Function</h3><p>Lets use this approach to make computed signals as well.</p><pre><code>function SimpleComputedSignal&lt;T&gt;(computeFn: () =&gt; T) {
  const signal = (() =&gt; computeFn()) as (() =&gt; T)

  const listeners: (() =&gt; void)[] = [];

  signal.effect = (listener: () =&gt; void) =&gt; {
    listeners.push(listener);
    listener();
  };

  const notify = () =&gt; {
    listeners.forEach((listener) =&gt; listener());
  };

  // Automatically track dependencies and notify listeners
  const originalCompute = computeFn;
  computeFn = () =&gt; {
    notify();
    return originalCompute();
  };

  return signal;
}

const baseCount = UpdatedSimpleSignal(2);
const doubleCount = UpdatedSimpleComputedSignal(() =&gt; baseCount() * 2);

const baseCount = UpdatedSimpleSignal(2);
const doubleCount = UpdatedSimpleComputedSignal(() =&gt; baseCount() * 2);

console.log(`Double count: ${doubleCount()}`);

baseCount.set(4); 

console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 8"

baseCount.set(8);
console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 16

baseCount.set(16);
console.log(`Double count: ${doubleCount()}`); // Logs: "Double count: 32</code></pre><p>Github link: <a href="https://github.com/alex-migwi/custom-signals">Simple Signal Repo</a></p><h3>More reading:</h3><p><strong><a href="https://medium.com/@adrien.za/creating-callable-objects-in-javascript-fbf88db9904c">Creating Callable Objects in JavaScript</a></strong><a href="https://medium.com/@adrien.za/creating-callable-objects-in-javascript-fbf88db9904c"><br></a><em><a href="https://medium.com/@adrien.za/creating-callable-objects-in-javascript-fbf88db9904c">var obj = new CallableObject(); obj(args);</a></em><a href="https://medium.com/@adrien.za/creating-callable-objects-in-javascript-fbf88db9904c">medium.com</a></p><p><strong><a href="https://medium.com/front-end-weekly/javascript-design-patterns-ed9d4c144c81">JavaScript Design Patterns</a></strong><a href="https://medium.com/front-end-weekly/javascript-design-patterns-ed9d4c144c81"><br></a><em><a href="https://medium.com/front-end-weekly/javascript-design-patterns-ed9d4c144c81">Constructor Pattern</a></em><a href="https://medium.com/front-end-weekly/javascript-design-patterns-ed9d4c144c81">medium.com</a></p><p><strong><a href="https://exploringjs.com/js/book/ch_callables.html#kinds-of-functions">Callable values</a></strong><a href="https://exploringjs.com/js/book/ch_callables.html#kinds-of-functions"><br></a><em><a href="https://exploringjs.com/js/book/ch_callables.html#kinds-of-functions">(Ad, please don't block.) 27.1 Kinds of functions 27.2 Ordinary functions 27.2.1 Named function expressions (advanced)&#8230;</a></em><a href="https://exploringjs.com/js/book/ch_callables.html#kinds-of-functions">exploringjs.com</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Unstacked Labs Newsletter! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Level Up your Testing Game with Jest Spies and Asymmetric Matchers ]]></title><description><![CDATA[Leveraging Jest Spies and Asymmetric Matchers for Reliable and Robust Unit Tests]]></description><link>https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Mon, 05 May 2025 06:53:18 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/09254933-41d1-4593-9d1f-eb4ceeab1291_1280x853.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over my long career as a software engineer, unit testing involving third-party APIS, such as database calls, etc., has always proven challenging. And let&#8217;s be honest, it&#8217;s pretty rare to write an application where all functions are pure,  i.e., self-contained, and don&#8217;t interact with third-party APIS&#8212;a topic I would really like to explore at some point in the future. For more on this and other topics, stay subscribed.</p><p>Let&#8217;s take the following simple function that gets items from the database - Dynamodb. Using the AWS SDK (v3), this function would look something like this:</p><pre><code>const client = new DynamoDBClient();
const docClient = DynamoDBDocumentClient.from(client);

export async function getToDo(id: string) {
  const res = await docClient.send(
    new GetCommand({
      TableName: 'Todos',
      Key: {
        id: id,
      },
    }),
  );
  // Avoid assertions, whenever possible
  return res.Item as ExchangeRateDBObject;
}</code></pre><p>The above function will use the AWS SDK to call the Dynamodb API and retrieve a to-do item with a given ID from the database. As long as you don&#8217;t have permission issues, it works.</p><div><hr></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><p>On the other hand, writing unit tests for it might be tricky, as the test would require access to an actual AWS Account or a local version of Dynamodb, each with its own set of challenges.</p><p>There are several ways to handle this, which I won&#8217;t go into in this article, but one of my favourites is mocking the Dynamodb SDK (Or any other SDK). The problem I generally find with mocks is that developers do not check whether the mocked function was called correctly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Qb7d!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Qb7d!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 424w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 848w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 1272w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Qb7d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif" width="320" height="578.4615384615385" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:260,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3024901,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/140154376?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Qb7d!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 424w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 848w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 1272w, https://substackcdn.com/image/fetch/$s_!Qb7d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F697b6ff6-a01e-46e8-83d0-021367cc462f_260x470.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Jest Spies</h3><p>To give you an example, when we mock out our Document Client above, we not only need to respond with the correct return signature&#8212;for instance, in our case, we only care about the Item property&#8212;but we also need to ensure that we passed in the correct information to the client. </p><p>In our case, the table name and the key must be correct; otherwise, if incorrect, our function would not work in the real world.</p><p>This is where Jest Spies come in. <strong>The Jest spy method allows us to monitor the behaviour of other functions without changing the underlying code for the purpose of testing</strong>. With spies, we can observe a few things, such as the times it was called and the parameters or inputs it was called with.</p><p>So, for instance, to test the above function, we would need to spy on <code>docClient</code> send method, returning the necessary response for our function to use, as shown below:</p><pre><code>it('Should pass the correct Key to the DB', async () =&gt; {
  const spyOnDB = jest.spyOn(docClient, 'send').mockReturnValue({
    Item: mockToDo,
  } as any);

  await getToDo('123');
});</code></pre><p>Now, if we run the above test, it will succeed using the mock test, as shown below.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;4441f1f7-3e66-4658-8f02-8b9e3049e4b0&quot;,&quot;duration&quot;:null}"></div><p>But if you recall correctly, we have mocked (using the <code>jest.spyOn</code> method) out our SDK call, but we aren&#8217;t doing anything to verify that we are calling our SDK correctly, which was one of my gripes with mocks in the first place. </p><p>We can now use a number of methods from the Jest matchers, such as <code>toHaveBeenCalled</code> and <code>toHaveBeenCalledWith </code>to ensure that our SDK/ function we are spying on was called a number of times, once in our case and was with the correct inputs/parameters.</p><pre><code>expect(spyOnDB).toHaveBeenCalled();</code></pre><p>The above matcher checks that our SDK was called; it doesn&#8217;t check whether it was called once or twice. If we wanted to be specific, Jest provides a different matcher that you can use to ensure the number of calls - <code>toHaveBeenCalledTimes</code> method.</p><pre><code>expect(spyOnDB).toHaveBeenCalledTimes(1);</code></pre><p>If our SDK is called more than once or not called at all, the test will fail. Neat, right?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_-Dn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_-Dn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_-Dn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg" width="633" height="394" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:394,&quot;width&quot;:633,&quot;resizeWidth&quot;:633,&quot;bytes&quot;:43850,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.unstacked.dev/i/140154376?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_-Dn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_-Dn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3adce78-0134-452d-8a76-237956b8d2d7_633x394.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Next, we can check whether the SDK passed the correct parameters. For that, we will use the <code>toHaveBeenCalledWith</code> method to check whether the correct inputs were passed to our AWS SDK.</p><pre><code>expect(spyOnDB).toHaveBeenCalledWith({
    TableName: 'Todos',
    Key: {
      id: '123',
    },
});</code></pre><p>Unfortunately, while we care about the above input, the GetCommand class transforms our input and appends some metadata, which we don&#8217;t care about for our test, but is still important. Due to that, the above test will fail.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;283e59a8-b780-49e8-9893-83ac08f4184d&quot;,&quot;duration&quot;:null}"></div><p>As seen above, our SDK input contains much more information than what we are checking, with the information we need nested somewhere in there. This is where Jest comes to the rescue with another feature&#8212;asymmetric Matchers. </p><h3>Asymmetric Matchers in Jest</h3><p>Asymmetric Matchers are magical in Jest, <strong>as they allow us flexibility when matching and asserting results in Jest</strong>, such as partial matching, as we want. </p><p>For instance, let&#8217;s say we have a random ID generator that&#8217;s prefixed, e.g., <code>user_UUID_STRING</code>. We could mock out the ID generator function to return a predetermined string&#8212;deterministic behaviour is important for testing. Another option is to use asymmetric matchers to check whether the returned ID contains our prefix, as shown below:</p><pre><code>expect(res).toEqual({
    ...USER_DETAILS,
    // Ensure the id is a string and starts with "user_"
    id: expect.stringContaining("user_"),
    // Or ensure the date is in the correct format
    updatedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
});</code></pre><p>As you can see, we are ensuring that the ID starts with the user_ prefix and the updatedAt date is of the correct matchers. </p><p>Circling back to our previous example, we now know that we need to ensure that the object passed to our SDK spy contains the following object, as we don&#8217;t care about the metadata and other details that the GetCommand appends.</p><pre><code>{
    input: {
      TableName: 'Todos',
      Key: {
        id: '123',
      },
    },
}</code></pre><p>Jest provides an asymmetric matcher for just this situation - <code>expect.objectContaining</code>. And this will check the results to see whether it contains the object we pass in, instead of strictly checking it, as shown below:</p><pre><code>expect(spyOnDB).toHaveBeenCalledWith(
    expect.objectContaining({
      input: expect.objectContaining({
        TableName: 'Todos',
        Key: {
          id: '123',
        },
      }),
    }),
);</code></pre><p>In layman&#8217;s terms, we are doing a partial check, and as long as the fields that we specify exist within the results, our test will parse, as shown below:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;a9f8d469-d051-4f84-8bb0-576bee837b65&quot;,&quot;duration&quot;:null}"></div><p>On top of that, Jest provides a good number of asymmetric matchers that you can use to make your life a little bit easier. </p><p>Just keep in mind that you need to ensure you are testing the most critical aspect of your code, and don&#8217;t use this as a shortcut by overusing matchers such as <code>expect.anything</code> or <code>expect.any</code>, which can literally match any string.</p><p>You can learn more about Asymmetric Matchers <a href="https://jestjs.io/docs/expect#asymmetric-matchers">here</a>.</p><h2>Conclusion</h2><p>In conclusion, unit testing when dealing with third-party APIS can be challenging, especially when it comes to ensuring that mocks accurately reflect the interactions with the real SDKS. </p><p>By leveraging both Jest Spies and Asymmetric Matchers, developers can create more robust, reliable and more effective tests that not only verify the functionality of their own functions but also confirm that the SDKS are being called correctly with the right parameters. </p><p>This enhances the reliability of your tests and helps maintain the integrity of your application's behaviour. Remember, while mocking provides a useful abstraction layer, focusing on meaningful assertions that capture the critical aspects of your application when using asymmetric matchers is essential, making sure that critical elements are what they should be while ignoring everything else. </p><p>By doing so, you'll be well-equipped to navigate the complexities of unit testing in a world full of dependencies. Happy testing!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/p/level-up-your-testing-game-with-jest?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;1e47c9ff-2d0f-484d-a0f8-228b85062c43&quot;,&quot;caption&quot;:&quot;Over the last few years, Angular has grown through some significant and important changes. One of those changes was incorporating server-side rendering into Angular instead of a library like it used to be with Angular Universal, the predecessor of Angular SSR (&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Exploring Routes Rendering Modes in Angular&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:43214086,&quot;name&quot;:&quot;Maina Wycliffe&quot;,&quot;bio&quot;:&quot;Author of All Things Typescript and Google Developer Expert for Angular&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/623bca1e-4bc1-4d0c-a173-cc5337080e98_1151x1728.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-02-10T07:30:08.203Z&quot;,&quot;cover_image&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb19eae5-288d-4ef5-b14c-20277be47a1c_1280x853.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://newsletter.unstacked.dev/p/exploring-routes-rendering-modes&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:155994582,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:1,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Unstacked Labs&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0b14a71-2c8f-42ea-a8a8-8db004cc48a5_419x419.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div>]]></content:encoded></item><item><title><![CDATA[Organize Your Firebase Functions For Easier Deployments and Maintenance]]></title><description><![CDATA[When developing Firebase functions, it's common to have everything in a single repository - all the Firebase Functions for your project. This is usually fine when you are starting out.]]></description><link>https://newsletter.unstacked.dev/p/organize-your-firebase-functions</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/organize-your-firebase-functions</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Thu, 20 Mar 2025 11:35:05 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b254b095-35d2-4d0d-a7fb-5b518fea4d75_1280x853.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When developing Firebase functions, it's common to have everything in a single repository - all the Firebase Functions for your project. This is usually fine when you are starting out.</p><p>But as your project grows, this can start to have negative impacts, for instance, slow deployment as Firebase has to build large functions, upload and figure out what changed and didn't change.</p><p>On top of that, it also makes it very difficult for multiple teams to work on the same project, making it hard to deploy and maintain as teams work on their features.</p><h2><strong>Firebase Functions Codebases</strong></h2><p>So, how do you manage this? Firebase provides the concept of Firebase Functions Codebases, where Firebase functions can be organised into a collection in a way that makes sense to an organisation, say a team owns them or by feature, and maintained and deployed together.</p><p>The functions collections can be in different repositories or the same repository in a mono-repo setup using something like <a href="https://nx.dev/?ref=content.mainawycliffe.dev">NX</a> in combination with the <a href="https://github.com/mainawycliffe/nx-toolkits?ref=content.mainawycliffe.dev">NX generator for Firebase</a> that I built earlier this year.</p><p>Firebase codebase allows you to organise your Firebase functions collection in a way that makes sense to your organisation. This could be by functionality or teams and so, allowing you to maintain and deploy them together.</p><p>By default, firebase has a single default codebase, and your Firebase config file - <code>firebase.json</code> - it looks like this.</p><pre><code><code>{
  // ... other firebase services configurations i.e. hosting etc.
  "functions": [
    {
      "source": "dist/apps/functions",
      // default codebase
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "pnpm nx run functions:build"
      ]
    }
  ],
  "extensions": {}
}
</code></code></pre><p>We can configure a different codebase for our Firebase functions for a second collection of functions by modifying the codebase property of one of our functions.</p><p>For instance, in the above example, we can add a second functions collection with a different codebase by adding a second config option to the functions property inside our <code>firebase.json</code> config file.</p><pre><code><code>{
  // ... other firebase services configs
  "functions": [
    {
      "source": "dist/apps/functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "pnpm nx run functions:build"
      ]
    },
     {
      "source": "dist/apps/functions",
      "codebase": "codebase-2",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "pnpm nx run functions:build"
      ]
    }
  ]
}
</code></code></pre><p>In the case above, our different functions collections exist in the same repository - a mono-repo. We could have our functions collection exist in multiple repositories. In such cases, we would just need the only codebase inside the repository to be configured in our <code>firebase.json</code> config file.</p><pre><code><code>{
  // ... other firebase services configs
  "functions": [
     {
      "source": "dist/apps/functions",
      "codebase": "codebase-2",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "pnpm nx run functions:build"
      ]
    }
  ]
}
</code></code></pre><blockquote><p>NB: Ensure that the codebase property is correct, as this could have unforeseen consequences, such as deleting other functions in the codebase that weren't found inside the repository.</p></blockquote><h3><strong>Deploying your Functions</strong></h3><p>As always, <code>firebase deploy</code> and <code>Firebase deploy functions will deploy all Firebase functions within the current directory</code> in all codebases. On top of that, you can specify which codebase to deploy by using the <code>firebase deploy functions:codebase</code> command.</p><pre><code><code>firebase deploy functions:codebase</code></code></pre><p>If your function collections are distributed in multiple repos, you can just use the <code>firebase deploy</code> command. Firebase CLI will not prompt you to delete existing Firebase function collections from other codebases.</p><p>But suppose your function collections are in a mono repo setup. In that case, you might find it beneficial to have your target to a specific codebase instead of deploying all codebases within the mono repo.</p><pre><code><code>firebase deploy functions:codebase</code></code></pre><h3><strong>Firebase Functions NX Generator</strong></h3><p>For those of you using NX or want to use NX to manage your Firebase project, you might find it beneficial to try out the generator I created to create Firebase functions as NX apps, with full support for codebase - <a href="https://github.com/mainawycliffe/nx-toolkits/blob/main/packages/firebase/README.md?ref=content.mainawycliffe.dev">@nx-toolkits/firebase</a>.</p><p>Each Firebase functions codebase lives as an NX application, with all the benefits of NX, such as codesharing between codebases and caching. For more information, check out this <a href="https://mainawycliffe.dev/blog/supercharge-your-firebase-app-development-using-nx/?ref=content.mainawycliffe.dev">post</a> about my motivations as to why I built the generator.</p><h3><strong>How it works</strong></h3><p>First, install the @nx-toolkits/firebase generator using your favourite package manager.</p><pre><code><code>// npm
npm i  @nx-toolkits/firebase

// yarn
yarn add @nx-toolkits/firebase

//pnpm
pnpm add  @nx-toolkits/firebase</code></code></pre><p>To generate a function in the default codebase, just run the following command:</p><pre><code><code>nx g @nx-toolkits/firebase:functions</code></code></pre><blockquote><p><strong>NB:</strong> Please ensure that you have run Firebase initialisation in the root directory of your project. In the future, I would like to be able to handle this as well.</p></blockquote><p>This will create or override the default codebase application. To create an NX application with a different codebase, just run the following command:</p><pre><code><code>nx g @nx-toolkits/firebase:functions --codebase codebase-2</code></code></pre><p>And that's it. Running the <code>nx deploy</code> command targeting the app you just generated will only deploy functions of the correct codebase.</p><pre><code><code>nx run my-functions-app:deploy</code></code></pre><p>And that's it.</p><h2><strong>Conclusion</strong></h2><p>In this post, we looked at how we can use codebases in Firebase functions to organise our Firebase functions, making it easy to maintain and deploy as our app grows.</p><p>It's important for any project to be able to enhance effectiveness and ensure different features can be developed and maintained with minimal conflicts and speed.</p><p>Codebases make it easy for organisations to decide how to organise Firebase functions as it makes sense to them, be it in a mono repo setup or even multiple repositories, so as to achieve business goals.</p><h3><strong>Resources</strong></h3><ul><li><p><a href="https://mainawycliffe.dev/blog/supercharge-your-firebase-app-development-using-nx/?ref=content.mainawycliffe.dev">Supercharge your Firebase App Development using NX</a></p></li><li><p><a href="https://www.allthingstypescript.dev/p/the-typeof-and-keyof-operators-referencing?ref=content.mainawycliffe.dev">The typeof and keyof operators - referencing variable types in Typescript</a></p></li><li><p><a href="https://firebase.google.com/docs/functions/organize-functions?gen=2nd&amp;ref=content.mainawycliffe.dev">Organize multiple functions</a></p></li><li><p><a href="https://www.allthingstypescript.dev/p/using-zod-schemas-as-source-of-truth?ref=content.mainawycliffe.dev">Using Zod Schemas as a Source of Truth for Typescript Types</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Exploring Routes Rendering Modes in Angular]]></title><description><![CDATA[In Angular 19, Angular introduced Route Models and Hybrid Rendering for SSR, in this article, we explore Rendering Modes and why they can be very useful]]></description><link>https://newsletter.unstacked.dev/p/exploring-routes-rendering-modes</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/exploring-routes-rendering-modes</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Mon, 10 Feb 2025 07:30:08 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/bb19eae5-288d-4ef5-b14c-20277be47a1c_1280x853.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the last few years, Angular has grown through some significant and important changes. One of those changes was incorporating server-side rendering into Angular instead of a library like it used to be with Angular Universal, the predecessor of Angular SSR (<code>@angular/ssr</code>). </p><p>Now, every angular application, out of the box, by default, uses SSR, which is an opt-out feature. For most people, when building Angular apps, it either needs to be server-side rendered or not; for instance, a dashboard behind a login wall doesn&#8217;t need SSR, while an e-commerce site needs it. </p><p>However, there are applications where there are some pages that are behind a login wall, and some public routes. If we circle back to our e-commerce site example, checkout pages, order pages, etc., are such examples that are probably behind a login wall, while product pages are public.</p><p>If you enable SSR on such a route behind a login wall, and someone visits the page, Angular will render the login page (or wherever the Auth Guards redirect them). When the page is loaded, Angular will determine the user is logged in and redirect them back to the actual page, as shown below:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;df0fa69e-c4ef-4860-bcd5-2bddf759dbf4&quot;,&quot;duration&quot;:null}"></div><p>This means the user will see a flash on the login page before seeing the actual page. This happens because, in our case, the authentication context is only available on the browser, not the server, and hence, on the server, the user is not logged in, but on the browser, the user is.</p><p>If you encountered this, like I did, before Angular 19, you would have to disable SSR, as that&#8217;s not an optimal user experience. However, the Angular team introduced in Angular 19 the concept of render modes for individual routes, or what is called hybrid rendering. </p><p>Instead of all routes either being client-side rendered (CSRd) or server-side rendered (SSRd), you can now choose what to do for each route. On top of that, they also added the ability to perform static site generation (SSG) for individual routes.</p><p>Let&#8217;s see some code.</p><h2>Setting Up Server Routes Manually</h2><div class="pullquote"><p>Please note that this feature is in <a href="https://angular.dev/reference/releases#developer-preview">developer preview</a>, so use it cautiously. For more information on what this means, please check out the following link on Angular versioning <a href="https://angular.dev/reference/releases">here</a>.</p></div><p>Before we can go any further, ensure you are on Angular 19. Angular has introduced a concept for Server Routes in which you can declare what to do for certain routes. To take the example above, we have two routes, as shown below:</p><pre><code>export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
    canActivate: [redirectIfNotLoggedInGuard],
  },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [redirectIfLoggedInGuard],
  },
];</code></pre><p>We can add server route configurations for the above route configuration using the path and the render mode we need. I chose to store them next to each other, but they can also live in its server routes file - whatever floats your boat.</p><pre><code>...
import { RenderMode, ServerRoute } from '@angular/ssr';

...

export const serverRoutes: ServerRoute[] = [
  {
    path: '',
    renderMode: RenderMode.Client,
  },
  {
    path: 'login',
    renderMode: RenderMode.Server,
  },
];</code></pre><p>The render mode accepts one of three values, <code>RenderMode.Client</code>, <code>RenderMode.Server</code>, and <code>RenderMode.Prerender</code>. For our very simple example above, we are <code>RenderMode.Client</code> for the home page, so it will not be server-side rendered, and then <code>RenderMode.Server</code> for the login page, it will be server-side rendered. </p><p>Once we set up our routes, we finally need to provide the server routes in our app config so that Angular can use them. We achieve this by using the <code>provideServerRouting</code> function and passing in the server routes we configured.</p><pre><code>...
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRouting } from '@angular/ssr';
...

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideServerRouting(serverRoutes),
    provideRouter(routes),
    ...
  ],
};</code></pre><p>And that&#8217;s it. If we circle back to our example, we can see that the login page flash is completely gone.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;6406eb94-fc33-4ec7-a4d5-9e56cc9537d5&quot;,&quot;duration&quot;:null}"></div><p>You can find the code for the above application <a href="https://github.com/unstacked-labs/angular-router-render-modes-demo">here</a>.</p><h3>Default Rendering Mode</h3><p>You can also set the default render mode by using the Angular router catch-all (wildcard) route - <code>**</code>, as shown below:</p><pre><code><code>export const serverRoutes: ServerRoute[] = [
  {
    path: '**',
    renderMode: RenderMode.Client,
  },
  {
    path: 'login',
    renderMode: RenderMode.Server,
  },
];</code></code></pre><p>This will render all pages on the client side except the login page, which will be rendered on the server.</p><h3>Setting Up For New Projects</h3><p>Of course, this is Angular we are talking about. With the magic of schematics, you can automate the above process; you will only have to configure your routes. </p><p>If you are starting a new project, Angular provides a flag <code>--server-routing</code> that enables this for the new project when setting it up.</p><pre><code>ng add @angular/ssr --server-routing</code></pre><h3>Conclusion</h3><p>In this brief article, we looked at Rendering Modes in Angular and how we can use them to provide a better user experience for our users. Before this, your options were limited; you could either use SSR or not use SSR if it broke the UX of your users. But with render modes, you can enable SSR where it makes sense, Prerender pages that make sense and CSR pages that make sense, ensuring a much better user experience for our users.</p><p>That&#8217;s it from me, and until next time, happy coding.</p>]]></content:encoded></item><item><title><![CDATA[Boost your productivity by mastering oh my ZSH git aliases]]></title><description><![CDATA[We take a look at Oh My ZSH Git Aliases and how I use them to improve my developer experience and boost my productivity.]]></description><link>https://newsletter.unstacked.dev/p/boost-your-productivity-by-mastering</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/boost-your-productivity-by-mastering</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Wed, 06 Mar 2024 05:07:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/639b0a2c-5923-48d3-9af1-46c5b6dc0602_640x433.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Since I discovered Oh My Zsh a few years ago, it has been a god-sent gift to me. I believe it has impacted my productivity in a very positive way, for instance, with the history autocomplete plugin, I no longer have to keep hitting the up arrow until I get to the command I wanted, I can just start typing and Oh My Zsh will give me suggestion s based on my history. It&#8217;s just genius.</p><blockquote><p>PS: If you are looking to get started with Oh My Zsh, you can find the installation instructions <a href="https://github.com/ohmyzsh/ohmyzsh/wiki">here</a>.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LxzX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LxzX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LxzX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg" width="531" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:531,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LxzX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LxzX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0a61540-ad8d-4a95-94a2-15e17af0bb37_531x470.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I guess this is the one plugin I can probably not survive with, my flailing memory would suffer greatly to have to remember all my previous commands, even the ones I worked out myself. A close second plugin for my workflow and I believe vital for most developers is the git plugin. It&#8217;s just genius.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/subscribe?"><span>Subscribe now</span></a></p><p>Of course, with Oh My Zsh, you have an arsenal of plugins that you can call upon to do your bidding, such as the docker, node, and npm, to mention a few. You can learn more about Oh My Zsh plugins (installation and list of plugins) in the official wiki <a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins">here</a>.</p><p>For a long time, I and the git plugin, have become very close buddies. Want to rebase, push (force push), pull, etc, the plugin will autocomplete everything for you including the git branches, I never get the spelling right on the first try, heck I am even known for copy-pasting them. </p><p>I even adapted my naming of git branches, starting with the issue number, followed by the title slug so that I can get even faster autocomplete. Don&#8217;t even ask what I was naming them before.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zbnC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zbnC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zbnC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg" width="888" height="499" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:499,&quot;width&quot;:888,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zbnC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zbnC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc3c4b7-caca-4a82-8770-70f977a97c78_888x499.jpeg 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I digress</figcaption></figure></div><h3>Oh My Zsh Git Aliases</h3><p>A few months ago, I came across Oh My Zsh git aliases, and well this has transformed my workflow. I can type into my terminal a combination of letters (3 to 5 characters, sometimes 6) and accomplish the same thing I did with a whole sentence. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9cOx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9cOx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 424w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 848w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9cOx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg" width="500" height="528" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:528,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65239,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9cOx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 424w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 848w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!9cOx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62c5322d-585f-4b55-b040-f56c426bc81f_500x528.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I know, we as developers have been accused of being overpaid &#129335;&#127998; and lazy and I am playing up to my character incredibly well here. I will take any shortcut I can to do more, by doing less. On the other hand, I am a huge fan of a good developer experience and I will put anything in my toolbox that improves it, including Github Copilot - I guess I owe another article here as well.</p><h4>Examples</h4><p>To show you what I mean, I will demonstrate why I am a huge fan of Git Aliases and why you should start using them. First, with the command that I have always had many struggles with, not because I can&#8217;t memorize it, but because I have to remember the branch name - <code>git push -f branch-name</code>.</p><p>If you work with Git issues, you probably create branch names for your issues, which could be anything. So, after a rebase against your main branch, you need to push the changes remotely to the PR (for those of us using Github, but there are equivalents for other remove hosting services such as Gitlab), so you need to type the above command:</p><pre><code>git push -f whatever-the-****-your-branch-name-is</code></pre><p>If you have Oh My Zsh and the Git Plugin, you will probably use it to autocomplete the branch name you want to force push, which is quite helpful. But you can make your life even more easier by using the following git alias:</p><pre><code>ggf</code></pre><p>Yeah, it&#8217;s that simple and will force-push your current branch without you having to worry whether you got the correct branch name or not (<em>god forbid you are not force-pushing on the main branch </em>&#128584;). Magic. Now imagine this, you have a few branches that are behind that you want to rebase and force push remotely. This does make your life a little bit easier, doesn&#8217;t it?</p><p>And did I mention, that it even makes rebasing way easier? So, let&#8217;s say, you notice that your branch is behind the origin main branch, instead of checking out the main branch and pulling the changes, you can simply fetch the remote main and merge your branch against it, as shown below:</p><pre><code>git fetch origin
git rebase origin/main</code></pre><p>If you think typing the above is a lot of work, like me, you can simplify that by using the following Git Aliasis.</p><pre><code>gfo &amp;&amp; grbom</code></pre><p>The above two aliases will fetch the origin branches and rebase the current branch against the origin main branch. And now you can simply run <code>ggf</code> to force push remotely. </p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2LfF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2LfF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 424w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 848w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 1272w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2LfF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif" width="360" height="238" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:238,&quot;width&quot;:360,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1812727,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2LfF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 424w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 848w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 1272w, https://substackcdn.com/image/fetch/$s_!2LfF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febac7059-582d-4a47-94f7-25f50a5aa431_360x238.gif 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Impressed?</figcaption></figure></div><p>Yes, there is a learning curve, of course, you have to memorize the above Aliases, nothing good comes for free. But I believe it&#8217;s worth it and they will come naturally to you with time. </p><p>Another thing I want to emphasize is that as you go on your way to memorizing and becoming a 10x lazier developer, learn the underlying Git commands, and understand what each Alias you use maps to and what the command does, this way, you will know when not to use an Alias and when to find a different one.</p><p>So, if you hear me and want to start using Git Aliases with Oh My Zsh, where do I start? Here are some of the most useful links to installing Oh My Zsh and a cheat sheet for the Aliases to help you memorize them.</p><ul><li><p><a href="https://github.com/ohmyzsh/ohmyzsh/wiki">Installing Oh My Zsh</a></p></li><li><p><a href="https://kapeli.com/cheat_sheets/Oh-My-Zsh_Git.docset/Contents/Resources/Documents/index">Oh-My-Zsh Git Cheat Sheet - Kapeli</a></p></li><li><p><a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins">Plugins Wiki</a></p></li></ul><h3>Conclusion</h3><p>In this post, we took a look at how can use Oh My ZSH git aliases to boost our productivity and improve our developer experience, With git aliases we can accomplish more while writing less, this is especially useful when performing rebates and pushing changes across multiple branches where the actions are compressed into just a few characters. </p><p>I hope this post convinced you to add Git Aliases to your developer toolbox. </p><p>Thank you for reading and until next time, keep on learning.</p>]]></content:encoded></item><item><title><![CDATA[A deep dive into new control flow syntax for Angular (17)]]></title><description><![CDATA[We take a look at the new control flow syntax that was released with #Angular 17 and its benefits over the old syntax and the new tricks it packs.]]></description><link>https://newsletter.unstacked.dev/p/a-deep-dive-into-new-control-flow</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/a-deep-dive-into-new-control-flow</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Mon, 20 Nov 2023 11:08:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the release of Angular 17, I wanted to explore the control flow syntax in Angular and demonstrate its benefits. The new syntax, which was part of the release for v17 of Angular, alongside a torn of other features we are going to look at in the future, is a big deal and a huge departure from how we accomplished control flow in Angular.</p><p>Control flow is the order in which statements are executed by the computer in a script. We can use conditions (<code>if&#8230;else, switch</code> statements) to determine which statements to execute and which to skip when certain conditions are met. We can even repeatedly execute statements using loops.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/subscribe?"><span>Subscribe now</span></a></p><p>Angular is getting a new syntax for control flow, a major departure from what things were (I will refer to it as the old syntax), and still are, as the new control flow syntax is still in the developer preview. </p><p>First, let&#8217;s compare the new syntax with the old syntax.</p><h4>If Conditions</h4><p>Let&#8217;s say we want to show a section of our template if the conditions are true. With the old syntax, we would do it like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!q7I6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!q7I6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 424w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 848w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 1272w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!q7I6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png" width="964" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:308,&quot;width&quot;:964,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:54152,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!q7I6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 424w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 848w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 1272w, https://substackcdn.com/image/fetch/$s_!q7I6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a1d9cea-32c8-474a-885f-a607a1169e2d_964x308.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Old if&#8230;.else conditional syntax in Angular</figcaption></figure></div><p>But now, with the all-new syntax, this would look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0rj7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0rj7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 424w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 848w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1272w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png" width="554" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:308,&quot;width&quot;:554,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:46990,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0rj7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 424w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 848w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1272w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">New Angular if&#8230;else conditional syntax</figcaption></figure></div><p>Or can be further simplified to:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2bH1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2bH1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 424w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 848w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 1272w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2bH1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png" width="432" height="352" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:352,&quot;width&quot;:432,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41997,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2bH1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 424w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 848w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 1272w, https://substackcdn.com/image/fetch/$s_!2bH1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc64f00f8-7da9-4d6e-ae25-64d9053bbe92_432x352.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>For Loop</h4><p>What about for loops:</p><p><strong>Old</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v4h0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v4h0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 424w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 848w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 1272w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v4h0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png" width="538" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:308,&quot;width&quot;:538,&quot;resizeWidth&quot;:538,&quot;bytes&quot;:34892,&quot;alt&quot;:&quot;Angular Old For Loop Syntax&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Angular Old For Loop Syntax" title="Angular Old For Loop Syntax" srcset="https://substackcdn.com/image/fetch/$s_!v4h0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 424w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 848w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 1272w, https://substackcdn.com/image/fetch/$s_!v4h0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71395fc2-04cc-41e6-9921-f2eb8c8fba32_538x308.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">For Loop before V17</figcaption></figure></div><p><strong>New</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ouFm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ouFm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 424w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 848w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 1272w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ouFm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png" width="561" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:308,&quot;width&quot;:561,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40493,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ouFm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 424w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 848w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 1272w, https://substackcdn.com/image/fetch/$s_!ouFm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9801c481-76fa-47d8-bad2-39eaf976e7c5_561x308.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">New Angular For Loop Syntax</figcaption></figure></div><p>As you can see, we are also passing a tracking expression that yields a unique key that we can use to associate the array items and their place in the DOM for performance reasons. This is required in the new control flow syntax, while before with the old syntax, it was optional.</p><h4>NgSwitch</h4><p>Here is an example of the old syntax</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F06H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F06H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 424w, https://substackcdn.com/image/fetch/$s_!F06H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 848w, https://substackcdn.com/image/fetch/$s_!F06H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 1272w, https://substackcdn.com/image/fetch/$s_!F06H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F06H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png" width="913" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:308,&quot;width&quot;:913,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:66128,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F06H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 424w, https://substackcdn.com/image/fetch/$s_!F06H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 848w, https://substackcdn.com/image/fetch/$s_!F06H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 1272w, https://substackcdn.com/image/fetch/$s_!F06H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F804ffe54-2da4-433d-9b84-b0b54472837e_913x308.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here is what that looks like now, with the new syntax</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4FiZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4FiZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 424w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 848w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 1272w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4FiZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png" width="690" height="379" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/33caea68-983e-46d0-aecd-5769caa79566_690x379.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:379,&quot;width&quot;:690,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:62505,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4FiZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 424w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 848w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 1272w, https://substackcdn.com/image/fetch/$s_!4FiZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33caea68-983e-46d0-aecd-5769caa79566_690x379.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Notice something? The new syntax is more readable (I know, it&#8217;s subjective, but I think we can both subjectively agree) and familiar, it looks like the very familiar syntax you would come across while writing Typescript or Javascript (or most languages for that matter). If you are new to Angular, good luck understanding the old syntax without a few head scratches and squinting your eyes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_y2W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_y2W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_y2W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg" width="577" height="433" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:433,&quot;width&quot;:577,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:56884,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_y2W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_y2W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F51cabae2-ba9a-4795-91dd-f690fad6bcb7_577x433.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And then there is another huge benefit, <strong>syntax highlighting and formatting</strong>. In the old syntax, we didn&#8217;t have much in the syntax highlighting corner, as the structural directive we part of the HTML attribute. Since now the control flow isn&#8217;t part of the HTML tags, syntax highlighting is already available. On top of that, the prettier npm package (update to the latest version) now supports formatting of the new Angular syntax and it&#8217;s just glorious. </p><p>Combine these two, and you can now easily tell where one block ends and the other one starts and any nested blocks within the template are easy to identify. This should aid in code readability and improve it exponentially.</p><p>And did I mention there is no more unnecessary <code>ng-container</code> and <code>ng-template </code>for conditional HTML blocks? This leads to much cleaner code with less boilerplate.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3tEn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3tEn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3tEn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg" width="620" height="455" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:455,&quot;width&quot;:620,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53997,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3tEn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3tEn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479c685f-4841-4711-bfe5-ee85b737d077_620x455.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I wish things were that simple</figcaption></figure></div><p>So far we have seen if and for syntaxes, what about the switch, here is an example.</p><h3>Improvements over the Old Syntax</h3><h4>&#8594; for loop: required track expression</h4><p>With the new syntax, providing a track expression that yields a key to keep track of each item in the array to the view location in the DOM for improved performance, especially over large lists is required. </p><p>Trying to leave it out, you get the following error:</p><pre><code>@for loop must have a "track" expression</code></pre><p>On top of that, Angular is using a new optimized algorithm for the for loop so that it&#8217;s more performant by making DOM operations as a response to collection changes minimal and hence more efficient.</p><h4>&#8594; for loop @empty Keyword</h4><p>On top of that, we now have an <code>@empty</code> keyword that we can use to handle situations where the list is empty, which is kind of neat. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JwzD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JwzD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 424w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 848w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 1272w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JwzD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png" width="1053" height="406" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:406,&quot;width&quot;:1053,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:70215,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JwzD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 424w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 848w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 1272w, https://substackcdn.com/image/fetch/$s_!JwzD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F470caf3e-ce74-4684-8c9e-248d5f8134eb_1053x406.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>&#8594; readable else</h3><p>As we saw earlier, doing else in the old control flow syntax in Angular was not really readable and required a lot of boilerplate code, however with the new control flow, it&#8217;s much more readable and more familiar, especially for developers just starting out in Angular. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0rj7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0rj7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 424w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 848w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1272w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png" width="554" height="308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:308,&quot;width&quot;:554,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0rj7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 424w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 848w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1272w, https://substackcdn.com/image/fetch/$s_!0rj7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cfc9a4-2cd2-4bae-a62d-5b353c0a4702_554x308.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What about the Async Pipe?</h3><p>Just like before, we can still the async pipe to subscribe to observables just like before.</p><p>Within for loops, this is how we can achieve this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9dRv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9dRv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 424w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 848w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 1272w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9dRv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png" width="1456" height="521" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:521,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:124683,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9dRv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 424w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 848w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 1272w, https://substackcdn.com/image/fetch/$s_!9dRv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17df9409-02a2-40da-a2ee-f7308c80cd64_1815x649.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The same can be done for the if blocks:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IYQf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IYQf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 424w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 848w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 1272w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IYQf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png" width="1194" height="433" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:433,&quot;width&quot;:1194,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:78856,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IYQf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 424w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 848w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 1272w, https://substackcdn.com/image/fetch/$s_!IYQf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75b11243-a7f6-48c5-97d2-504388f7a6bd_1194x433.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>I am sold, how do I switch?</h3><p>First, make sure you have updated your Angular project to v17 and then you can run the following schematic to convert existing control flow syntax to the new control flow syntax.</p><pre><code><code>ng g @angular/core:control-flow-migration</code></code></pre><p>When prompted for a path, enter the path to your project and that&#8217;s it.</p><p>A word of caution, which is a huge step forward in Angular, is that the control flow syntax is in the <a href="https://angular.io/guide/releases#developer-preview">developer preview</a>. This means some things may change under the hood without following semantic versioning, as it gives the Angular team flexibility to move first and fix issues and concerns that may arise before it&#8217;s generally available with the same <a href="https://angular.io/guide/releases">guarantees Angular</a> provides for all its features.</p><h3>Next on Unstacked: The all-new @defer syntax</h3><p>On top of that, Angular is getting a new <code>@defer</code> syntax that can be used to lazy load components, directives, and pipes in the template, until certain conditions are met. I am going to go over this in the next issue of Unstacked in the next couple of weeks as this post is becoming overly long for a newsletter.</p><h3>Conclusion</h3><p>In this post, we took a look at the new control flow syntax for angular and the benefits it brings along as compared to the old syntax. We learned that the new control flow syntax is more familiar and subjectively more readable as compared to the old one, and requires less mental gymnastics to understand what&#8217;s going on, even for experienced Angular devs, let alone newbies. We also learned how the new syntax not only replaces the old syntax but improves upon it by bringing in extra helpers such as <code>@empty</code> and required track by expression for improved performance.</p><p>That&#8217;s it from me, and until next time, keep on learning.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/p/a-deep-dive-into-new-control-flow?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/p/a-deep-dive-into-new-control-flow?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[Web Performance - Key metrics to ensure your website is Performant.]]></title><description><![CDATA[In this issue, we are going to go through on why you should care about the performance of your website and important metrics you can use to measure performance and improver user experience.]]></description><link>https://newsletter.unstacked.dev/p/web-performance-key-metrics-to-ensure</link><guid isPermaLink="false">https://newsletter.unstacked.dev/p/web-performance-key-metrics-to-ensure</guid><dc:creator><![CDATA[Maina Wycliffe]]></dc:creator><pubDate>Sat, 04 Nov 2023 11:15:54 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b75b56a2-88b2-4f0a-9d76-0a227992cfb3_1280x853.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the next few weeks, I will be speaking at Google DevFest events regarding measuring your website performance. After previous talks that I gave, I have come to the realization that having an article that goes into detail in relation to the topic is important, as it gives attendees a place where they can learn more about the topic that&#8217;s better than slides.</p><p>On top of that, I also thought this would be a great article for my first-ever post here at Unstacked, a new newsletter that appeals to a broader developer audience, in contrast to my other newsletter - <a href="https://allthingstypescript.dev/">All Things Typescript</a> (whispers - subscribe &#128521;).</p><p>Without wasting any more of your time, let&#8217;s dive into today&#8217;s issue about web performance and key metrics that you should always keep an eye on.</p><div class="pullquote"><p>Web performance is all about making websites fast, including making slow processes <em>seem</em> fast.</p><p><a href="https://developer.mozilla.org/en-US/docs/Learn/Performance/What_is_web_performance">MDN Docs</a></p></div><h3>Agenda</h3><ul><li><p>Why care about your website performance</p></li><li><p>Key Metrics to use to measure your Website Performance </p><p></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/subscribe?"><span>Subscribe now</span></a></p><h3>Why?</h3><p>Websites are a key component of our day-to-day lives. They connect us to vital information, and key services such as Government services, connect with people, and do business among so many things. This means it&#8217;s essential that our website provide a decent user experience.</p><p>The problem that exists is that while our website may behave as expected on our devices, our users won&#8217;t be accessing the device from our devices, but their own and under different circumstances.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JL9w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JL9w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JL9w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg" width="600" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:600,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:261193,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JL9w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!JL9w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06f993cf-ec1a-4a31-92fe-9cbcb5c3b93d_600x500.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This reminds me of this tweet I came across last year, which sadly I didn&#8217;t save, which had the following quote:</p><div class="pullquote"><p>Your iPhone isn&#8217;t the real world</p></div><p>It&#8217;s important as web developers to remember that outside our comfort zone (developers are mostly well paid with access to the latest technologies at their hands), our users are not going to be in ideal locations to access our websites hence the above quote.</p><p>For instance in Africa, most people have budget smartphones, with low memory and CPU processing capabilities, and also a gotcha, and these people also live in areas with poor network connectivity such as rural areas.</p><p>As developers, we need to ensure that our websites have a decent if not great user experience for users with the cheapest devices and poor network conditions - the lowest common denominator as well for people with more expensive devices and great internet connection.</p><h2>Metrics</h2><h3>Web Vitals</h3><p>So, how do you know a website is performing well or poorly? How do you measure the performance of a website? This is where Web Vitals comes in, an initiative by Google to provide unified guidance for quality signals that are essential to delivering a great user experience on the web.</p><p>Web Vitals are metrics such as how long it takes for the user to see content on the web page they visited that we can use to understand how well your website is doing. If your website has good performance in these metrics, then the user experience is most likely great, otherwise poor.</p><h3>Core Web Vitals</h3><p>We are not going to go into all Web Vitals in this issue, I will leave that up for a future issue. Core Web Vitals are a subset of Web Vitals (goes without saying) and apply to all websites. Each vital in the Core Web Vitals measures important and distinct user experience metrics and reflects the real-world performance of your website, and the list will evolve over time, changing as needed.</p><p>As of the time of writing this issue, here are the core web vitals:</p><h4>Largest Contentful Paint (LCP)</h4><p>The Largest Contentful Paint (LCP) metric measures the render time of the largest block (text or image) that is visible within the viewport, from the point the page starts to load. This considers images, videos, text blocks, the first frame of animated images, and auto-playing videos, etc.</p><p>For your site to have a good LCP score, you need to achieve a score of 2.5s or less, ideally in 75% of all page loads on your websites.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mtMT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mtMT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mtMT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Good LCP values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Good LCP values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement" title="Good LCP values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement" srcset="https://substackcdn.com/image/fetch/$s_!mtMT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!mtMT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97741b63-dc29-467d-b7a5-b175518fc999_384x288.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Source: web.dev</figcaption></figure></div><p><strong>Optimize for the Largest Contentful Paint</strong></p><p>When you measure LCP either using Lighthouse or <a href="https://pagespeed.web.dev/">PageSpeed Insight</a>, it usually provides a list of reasons why your LCP was given a certain score and how you can optimize for it.</p><p>If you have a website with a poor LCP score, here are some of the things you can do to optimize for LCP.</p><p><strong>Improve your server response time</strong></p><p>For images, CSS, javascript, HTML, and static files, consider using a CDN, with good caching policies as seen fit. If your server is slow, it impacts another metric called <a href="https://web.dev/articles/ttfb">Time To First Byte</a> (TTFB), which measures the time it takes for a request to get the first byte of the response. This is a subset metric for Largest Content Paint (LCP) and First Contentful Paint (FCP).</p><p><strong>Optimize for Resource Discovery on your Web Page</strong></p><p>Resources impacting LCP should always be loaded as early as possible. Your browser ships with a <a href="https://web.dev/articles/preload-scanner">preload scanner</a>, which will go through your raw HTML before it&#8217;s parsed by the browser and look for items it can prefetch such as images, and <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload">rel=preload</a> Links.</p><p>To optimize for resource discovery:</p><ul><li><p>ensure that all images have the image URL set inside <code>srset</code> or <code>src</code> attributes for images, don&#8217;t use javascript to set image src path for important resources, and </p></li><li><p>CSS Background Images and Fonts are preloaded using the <code>&lt;Link rel=preload /&gt;</code>.</p></li></ul><p><strong>Give Priority to Important Resources to Loaded Upfront</strong></p><p>After optimizing for discovery, it is important to ensure discovered resources are loaded before non-important ones. Resources such as images, that are non-blocking aren&#8217;t usually given priority by the browser even when they are discovered by the preload scanner. We can use the <a href="https://web.dev/articles/fetch-priority">fetch-priority</a> attribute to hint to the browser that an image is important and should be given priority. </p><pre><code>&lt;img fetchpriority="high" src="/path/to/important/image.webp"&gt;</code></pre><p>We can also de-prioritize resources, to ensure that we are not taking too much of the browser bandwidth and have enough of it to load important resources.</p><p><strong>Others Optimization</strong></p><ul><li><p>Use modern image formats such as WebP. You can use a specialized image CDN that will automatically serve an image format compatible with the user's device.</p></li><li><p>Use Responsive Images. Don&#8217;t use one large image for all screen sizes, but instead use <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images">responsive images</a>.</p></li><li><p>Compress Images to the smallest possible size, without impacting the quality too much.</p></li><li><p>Use appropriate cache settings ensuring that network latency is reduced and only necessary for the initial load of the resource</p></li><li><p>Remove any unnecessary CSS and Javascript for a page</p></li><li><p>Reduce font size</p></li></ul><h3>Cumulative Layout Shift (CLS)</h3><p>One of the most frustrating things to users is when the content they are reading suddenly moves to a different location. This could be caused by things such as an image loading above the content, pushing the content down or some other content being added dynamically such as ads and the content has to move to create space for it.</p><p>This is quite difficult to see as a developer as we are usually lucky enough to be using fast or decent-speed internet, images are already cached, which reduces the load time for when we are testing.</p><p>CLS is an important metric as it measures unexpected layout shifts during the lifespan of  a webpage. A layout shift occurs anytime visible elements to the user suddenly change positions and CLS helps us understand how often our web page is doing this. </p><p>There are two types of layout shifts to keep in mind:</p><ul><li><p>unexpected layout shift - it wasn&#8217;t intended by the developer as part of the web page design and is bad,</p></li><li><p>and the expected layout shift - think of user-initiated layout shift, like when they click a button to expand a section or animated content.</p></li></ul><p>Ideally, you should have zero layout shifts on your web page, but a good score is 0.1 or less for 75% of all page loads on your website.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OFPg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OFPg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OFPg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Good CLS values are 0.1 or less, poor values are greater than 0.25, and anything in between needs improvement&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Good CLS values are 0.1 or less, poor values are greater than 0.25, and anything in between needs improvement" title="Good CLS values are 0.1 or less, poor values are greater than 0.25, and anything in between needs improvement" srcset="https://substackcdn.com/image/fetch/$s_!OFPg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!OFPg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba1ba12-92ca-4f2f-b749-5a0e02aeda99_384x288.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Source: web.dev</figcaption></figure></div><p>To optimize for CLS, ensure that all your images have a fixed height and width, or add <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio">aspect ratio</a> to them, so that the browser can reserve a place for them in the rendered page.</p><h3>First Input Delay (FID)</h3><p>Ever visited a web page and after it loaded, and you tried to click on a link or button and it took a while for the website to respond? First Input Delay (FID) measures this specific metric of how long it takes for the web page to respond to user interaction such as clicks, taps, and keyboard events during the loading process. For a good FID score, it should have a delay of 100 milliseconds or less.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iWJu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iWJu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iWJu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Good FID values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Good FID values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement" title="Good FID values are 2.5 seconds or less, poor values are greater than 4.0 seconds, and anything in between needs improvement" srcset="https://substackcdn.com/image/fetch/$s_!iWJu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 424w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 848w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 1272w, https://substackcdn.com/image/fetch/$s_!iWJu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3effc3a5-8287-4d6e-8a28-9987764e4735_384x288.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Source: web.dev</figcaption></figure></div><p>In order to optimize for FID, you can do the following:</p><ul><li><p>Reduce or minimize the Javascript that&#8217;s initially loaded by your application during the loading process.</p></li><li><p>Offload tasks from the main threads, i.e. use Web Workers to perform heavy computations. The main thread is blocking and when the browser is executing Javascript in the main thread, it will not process any user interactions.</p></li><li><p>Explore on-demand loading of Scripts as if you load too much on the main thread, it will slow down user interactions.</p><ul><li><p>This can be done by deferring unused and unimportant javascript scripts. This can be achieved by adding the defer tag to your scripts. You can learn more about both def and async attributes <a href="https://javascript.info/script-async-defer">here</a>.</p></li><li><p>Code-splitting or lazy loading and loading the smaller chunks on demand.</p></li><li><p>And finally, minimize the use of pollyfills.</p></li></ul></li></ul><h3>Interaction to Next Paint (INP)</h3><p>Ever found yourself on a very sluggish website, where the page was slow to respond and even freezes once in a while? Interaction to Next Paint or INP is a metric that&#8217;s designed to measure the general interactivity of your website. Unlike the <strong>First Input Delay</strong> (FID) metric that focuses on time to first interaction, this focuses on all interactions on your website.</p><p>INP measures the latency of keyboard interactions, taps, and clicks during the lifespan of the site. The value of the longest interaction is the INP value, with any outliers ignored. A good INP is below 200 milliseconds.</p><p>You can do the following to optimize your INP score:</p><ul><li><p>Remove unnecessary Javascript from your webpage as it impacts start-up performance. The browser has to download and parse all of the Javascript in order to provide interactivity, and if it&#8217;s too much, it means the first interaction will take a bit longer. You can achieve this by using techniques such as Lazy Loading where javascript is broken up into smaller chunks and only necessary Javascript is loaded up.</p></li><li><p>Avoid having heavy and long-running tasks on the main thread as they can block other processes hence impacting the performance of your site.</p></li><li><p>Minimize the size of the DOM - with large DOM elements, the rendering work takes longer and hence impacts the performance of your Website. With large DOM elements, both the initial rendering and any rerendering afterward are going to be very expensive and slow. </p><ul><li><p>You can use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility">content visibility</a> to control whether an element's content is rendered or not. </p></li><li><p>Other techniques include the use of <a href="https://www.patterns.dev/posts/virtual-lists">virtual lists</a> where we only render visible parts of lists instead of everything at once, hence improving performance.</p></li></ul></li><li><p>Be aware of the cost of Javascript rendering HTML and CSS. When your browser receives HTML, it can stream it and parse it chunk by chunk, ensuring the best performance. However, with the advent of modern frameworks such as React, Angular, Vue, etc. it&#8217;s common to ship a javascript site that then renders the HTML and sometimes even the CSS. This means your browser has to download all the necessary Javascript content, in this case, it will your whole application code (all pages), and then parse it, and only then you will see HTML content. This is very expensive and slow, and we have come up with techniques such as:</p><ul><li><p>Server Side Rendering (SSR) where this work is done on the server instead so we can send HTML to the client, </p></li><li><p>and Lazy Loading, where we break up our application into smaller chunks that can be loaded as needed. As developers, however, we need to be aware of this and avoid it where necessary.</p></li></ul></li></ul><h2>Conclusion </h2><p>In this issue, we talked about Web Performance and important metrics to keep an eye on in order to ensure the user experience is decent if not great. We learned that it&#8217;s important to ensure that the lowest common denominator users get a good user experience and can use your website without struggling too much. It&#8217;s important for web developers to ensure a website with great performance and great user experience.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://newsletter.unstacked.dev/p/web-performance-key-metrics-to-ensure?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://newsletter.unstacked.dev/p/web-performance-key-metrics-to-ensure?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p> </p><p></p>]]></content:encoded></item></channel></rss>