<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Mon, 05 Oct 2026 12:30:12 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=sumeyye.kurtulus" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/state-without-a-circuit-tempdata-and-session-in-blazor-.net-11-static-ssr-q8r6l6ug</guid>
      <link>https://abp.io/community/posts/state-without-a-circuit-tempdata-and-session-in-blazor-.net-11-static-ssr-q8r6l6ug</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>state-management</category>
      <category>security</category>
      <category>aspnet-core</category>
      <category>.net</category>
      <title>State Without a Circuit: TempData and Session in Blazor .NET 11 Static SSR</title>
      <description>A practical, deep-dive guide for .NET developers on managing state in Blazor .NET 11 Static SSR using [SupplyParameterFromTempData] and [SupplyParameterFromSession] explaining real-world PRG patterns, multi-step wizards, security, scale-out, and browser matrix verification without SignalR circuits.</description>
      <pubDate>Fri, 02 Oct 2026 12:08:35 Z</pubDate>
      <a10:updated>2026-10-05T12:27:21Z</a10:updated>
      <content:encoded><![CDATA[<h1>State Without a Circuit: TempData and Session in Blazor .NET 11 Static SSR</h1>
<blockquote>
<p><strong>.NET 11 RC1.</strong> The attributes and defaults in this article come from the ASP.NET Core 11 release notes and the Blazor server state-management docs: <code>[SupplyParameterFromTempData]</code>, <code>[SupplyParameterFromSession]</code>, cookie TempData, and cookie-backed session. They apply to <strong>static server-side rendering only</strong>. Interactive Server and WebAssembly leave the property at its CLR default. The browser matrix at the end is the contract to check on the sample, <a href="https://github.com/sumeyyeKurtulus/StaticSsrState">StaticSsrState</a>. It is not a record of a browser run.</p>
</blockquote>
<p>A static SSR page is one HTTP request. The component is created, rendered, and thrown away. Nothing you stored in a field is there for the next click. That is a good trade when you do not want a SignalR circuit per user, and it is a problem the moment a form needs to say &quot;Saved.&quot; after a redirect, or a three-step wizard needs to remember step one.</p>
<p>.NET 11 gives Blazor static SSR the two stores MVC and Razor Pages have used for years, wired as component parameters:</p>
<ul>
<li><code>[SupplyParameterFromTempData]</code> — read-once flash state. The Post-Redirect-Get message.</li>
<li><code>[SupplyParameterFromSession]</code> — state that must survive several requests. The wizard draft or the cart.</li>
</ul>
<p>Both are declarative in the same way as <code>[SupplyParameterFromQuery]</code> and <code>[SupplyParameterFromForm]</code>. You stop injecting <code>IHttpContextAccessor</code> and hand-serializing a session key for every property.</p>
<p>Six rules cover the choice. The sections after them are the code, the limits, and how to confirm each rule in the browser.</p>
<ul>
<li><strong>TempData is the flash.</strong> It is written on the POST and read on the next GET. The attribute's first read uses <code>ITempData.Get()</code>, which schedules deletion. Copy the value into a display field and clear the parameter before the request ends, because the framework writes the property back at the end of the request.</li>
<li><strong>Session is the multi-request store.</strong> Values stay until the idle timeout (20 minutes by default; each access resets it), until you assign <code>null</code>, or until the session cookie disappears.</li>
<li><strong>Static SSR only.</strong> On an interactive render mode the same property is never supplied.</li>
<li><strong>Different boxes.</strong> TempData's default store is an encrypted cookie, <code>.AspNetCore.Components.TempData</code>. Session puts the payload in a server-side cache and sends only <code>.AspNetCore.Session</code>.</li>
<li><strong>The URL is still the shareable store.</strong> A copied link does not carry TempData or Session. Bookmarkable view state belongs in the query string.</li>
<li><strong>One provider for TempData.</strong> The cookie provider is the default. Switching to session storage removes the cookie size cap and requires session affinity. Cookie-backed TempData cannot be saved after the response starts streaming.</li>
</ul>
<h2>Where this state should live</h2>
<p>Pick the store from the lifetime you actually need. The rest of the article is the two middle rows.</p>
<p>| Store                                                | Lifetime                                   | Shareable link               | Survives F5                      | New tab                  | Static SSR                          | What you operate                                                              |
| ---------------------------------------------------- | ------------------------------------------ | ---------------------------- | -------------------------------- | ------------------------ | ----------------------------------- | ----------------------------------------------------------------------------- |
| TempData                                             | Next read, then removed                    | No                           | No, once the GET has consumed it | No                       | Yes                                 | Encrypted cookie, or the session store if you switch provider                 |
| Session                                              | Idle timeout, reset on access              | No                           | Yes                              | Yes, same session cookie | Yes                                 | Server cache plus a session cookie; session affinity required                  |
| URL query                                            | As long as the URL is kept                 | Yes                          | Yes                              | Yes                      | Yes                                 | Nothing on the server                                                         |
| Database                                             | Until you delete the row                   | Only if the id is in the URL | Yes                              | Yes                      | Yes                                 | Your database                                                                 |
| Interactive Server, WASM, <code>PersistentComponentState</code> | Circuit, prerender handoff, or the browser | No                           | Depends on the mode              | No                       | The page no longer has to be static | A circuit or a client runtime                                                 |</p>
<p>A success banner after save is TempData. A wizard or a cart that must not show up in the address bar is Session. A filtered list someone should send to a colleague is the query string and that is the subject of <a href="https://abp.io/community/articles/quickgrid-in-.net-11-sorting-and-paging-that-live-in-the-url-hypzmikw#gsc.tab=0">QuickGrid in .NET 11: Sorting and Paging That Live in the URL</a>. An order that must still exist after the browser is closed is a database row.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/c9a3ff76090326ab491d65082ecde1be5b03df44/docs/en/Community-Articles/2026-10-02-State-Without-a-Circuit%3A-TempData-and-Session-in-Blazor-.NET-11-Static-SSR/images/prg-session-flow.svg" alt="A POST writes the flash into cookie TempData and the wizard draft into the server session. The redirect GET reads both. Refresh drops the flash and keeps the draft." /></p>
<h2>What a static SSR request actually keeps</h2>
<p>Blazor Server holds component fields in a SignalR circuit. WebAssembly holds them in the browser. Static SSR holds them nowhere. The next request starts from the parameters the framework can rebuild: route, query, form, TempData, session.</p>
<p>Circuit persistence, <code>[PersistentState]</code>, and auto-pause are interactive-server features. They resume a circuit. They do not implement Post-Redirect-Get on a page that has no circuit. The section <a href="#when-static-ssr-is-the-wrong-tool">When static SSR is the wrong tool</a> comes back to them.</p>
<h3>TempData</h3>
<p>TempData is the bag you fill on the POST so the redirect target can show a message once. <code>AddRazorComponents()</code> registers it. You do not call <code>AddSession</code> for the default cookie provider.</p>
<p>The full API is a cascading <code>ITempData</code>:</p>
<pre><code class="language-csharp">[CascadingParameter]
public ITempData? TempData { get; set; }
</code></pre>
<p><code>Get</code> reads a key and schedules it for deletion. <code>Peek</code> reads it and leaves it. <code>Keep()</code> retains every key for the following request. <code>Keep(key)</code> retains one. Keys are case-insensitive. Those methods are how you keep a message alive across an intermediate redirect. The attribute does not expose them — the TempData attribute work left <code>Peek</code> and <code>Keep</code> on <code>ITempData</code> on purpose.</p>
<p>For a single value, <code>[SupplyParameterFromTempData]</code> is the shortcut. The key defaults to the property name. Set <code>Name</code> when the key should be stable across a rename, or when two properties would otherwise collide:</p>
<pre><code class="language-csharp">[SupplyParameterFromTempData]
public string? Message { get; set; }

[SupplyParameterFromTempData(Name = &quot;flash_message&quot;)]
public string? FlashMessage { get; set; }
</code></pre>
<p>Two components in the same render tree cannot register the same key. The supplier throws <code>InvalidOperationException</code>. Mixing <code>TempData[&quot;flash_message&quot;]</code> and the attribute for that same key in one request is unsupported; the value that wins depends on order.</p>
<p>The first time the attribute reads a key it calls <code>Get()</code>, so the key is marked for deletion. Later reads in the same request return the property's current value, so your submit handler can overwrite it. At the end of the request the supplier writes every bound property back into TempData. That write-back is why a flash you leave sitting in the property can be saved again. The feedback sample below copies the string into a display field and assigns <code>null</code> before the response completes.</p>
<p>The supported types are a closed list: <code>string</code>, <code>int</code>, <code>bool</code>, <code>Guid</code>, <code>DateTime</code>, int-backed enums, the nullable forms of those, <code>T[]</code>, <code>List&lt;T&gt;</code>, <code>HashSet&lt;T&gt;</code>, <code>Collection&lt;T&gt;</code>, <code>Dictionary&lt;string, T&gt;</code>, and <code>object[]</code>. A custom class is not on that list.</p>
<p>A read-side mismatch is the case that becomes <code>null</code>. If the stored value's type is not assignable to the property, or deserialization fails, the attribute supplies <code>null</code> and writes a log entry. An unsupported non-null value fails later, when the response is persisted, after the component has rendered.</p>
<p>The default provider stores that JSON in an encrypted cookie. Data Protection does the encryption. The docs' defaults are:</p>
<p>| Setting      | Default                           |
| ------------ | --------------------------------- |
| Name         | <code>.AspNetCore.Components.TempData</code> |
| HttpOnly     | <code>true</code>                            |
| SameSite     | <code>Lax</code>                             |
| SecurePolicy | <code>SameAsRequest</code>                   |</p>
<p>On a production HTTPS site, set <code>CookieSecurePolicy.Always</code>. Override the cookie on the Razor components options:</p>
<pre><code class="language-csharp">builder.Services.AddRazorComponents(options =&gt;
{
    options.TempDataCookie.Name = &quot;.AspNetCore.Components.TempData&quot;;
    options.TempDataCookie.HttpOnly = true;
    options.TempDataCookie.SameSite = SameSiteMode.Lax;
    options.TempDataCookie.SecurePolicy = CookieSecurePolicy.Always;
});
</code></pre>
<p>Browsers limit a single cookie to about 4 KB. The provider chunks large values with <code>ChunkingCookieManager</code>. Past that, call <code>AddSessionStorageTempDataValueProvider()</code>. Only one provider is active. Session-backed TempData then needs <code>AddSession</code>, <code>UseSession</code>, and session affinity. Cookie-backed TempData cannot be saved after the response has started streaming. A page that streams and still needs TempData has to use the session provider.</p>
<h3>Session</h3>
<p>Session is the store you read on several requests and never want in the query string: the onboarding draft, the cart id, the step index. Unlike TempData, a read does not delete it.</p>
<p>It is not registered by <code>AddRazorComponents()</code> alone:</p>
<pre><code class="language-csharp">builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =&gt;
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SameSite = SameSiteMode.Lax;
});

builder.Services.AddRazorComponents();

var app = builder.Build();

app.UseSession();
app.MapRazorComponents&lt;App&gt;();
</code></pre>
<p><code>UseSession()</code> has to run before the component endpoint. <code>UseAntiforgery()</code> is optional in .NET 11 and is no longer in the Blazor template. The defaults you are overriding, or accepting, are:</p>
<p>| Setting      | Default                          |
| ------------ | -------------------------------- |
| Cookie name  | <code>.AspNetCore.Session</code>            |
| Path         | <code>/</code>                              |
| HttpOnly     | <code>true</code>                           |
| SameSite     | <code>Lax</code>                            |
| SecurePolicy | <code>None</code>                           |
| IsEssential  | <code>false</code>                          |
| IdleTimeout  | 20 minutes, reset on each access |
| IOTimeout    | 1 minute                         |</p>
<p><code>IsEssential</code> defaults to <code>false</code>, so cookie-consent middleware can drop the cookie and the wizard silently restarts. The sample sets it to <code>true</code> because the draft is required for the flow. Idle timeout applies to the session contents, not to the cookie's <code>Expires</code>. <code>SecurePolicy</code> defaults to <code>None</code> because apps often mix HTTP and HTTPS in development, and some browsers refuse to overwrite a <code>Secure</code> cookie from an insecure URL. That rationale belongs to this cookie, not to TempData.</p>
<p><code>[SupplyParameterFromSession]</code> reads the key on the way in and writes the property back before the response is sent. Allowed values are the same closed list as TempData: <code>string</code>, <code>int</code>, <code>bool</code>, <code>Guid</code>, <code>DateTime</code>, int-backed enums, their nullables, <code>T[]</code>, <code>List&lt;T&gt;</code>, <code>HashSet&lt;T&gt;</code>, <code>Collection&lt;T&gt;</code>, <code>Dictionary&lt;string, T&gt;</code>, and <code>object[]</code>. A custom class is not on that list. <code>[SupplyParameterFromSession] OnboardingDraft? Draft</code> threw <code>InvalidOperationException</code>: the type is not supported for session storage. The sample stores <code>string?</code> and <code>int?</code> instead. A duplicate key across components throws <code>InvalidOperationException</code>. Keys are compared case-insensitively.</p>
<pre><code class="language-csharp">[SupplyParameterFromSession(Name = &quot;onboarding_name&quot;)]
public string? Name { get; set; }

[SupplyParameterFromSession(Name = &quot;onboarding_step&quot;)]
public int? Step { get; set; }
</code></pre>
<p><code>AddDistributedMemoryCache()</code> is process-local. Session requires session affinity in a load-balanced deployment, including when the cache is in memory. A second instance does not see the first instance's memory.</p>
<p>Streaming SSR has two separate rules. If the page subscribes with <code>[SupplyParameterFromSession]</code>, or the session-storage TempData provider is active, the session cookie is issued before streaming starts, even when the handler writes nothing. Pages that do not touch session are unchanged. Cookie-backed TempData cannot be saved once streaming has started, so a streaming page that needs TempData uses <code>AddSessionStorageTempDataValueProvider()</code> and takes on the same affinity requirement.</p>
<h3>What the attributes leave alone</h3>
<p>During interactive SSR and interactive client rendering the value is not supplied. The property stays at its default: <code>null</code> for <code>string?</code>, <code>null</code> for <code>int?</code>. A page marked <code>@rendermode InteractiveServer</code> that expects the wizard to appear will render the empty state.</p>
<p>They also do not replace antiforgery. <code>EditForm</code> with a <code>FormName</code> emits the token. A plain <code>&lt;form method=&quot;post&quot;&gt;</code> needs <code>&lt;AntiforgeryToken /&gt;</code> and <code>@formname</code>. Leave validation on. Calling <code>UseAntiforgery()</code> is optional in .NET 11, and the Blazor template no longer includes it.</p>
<h2>The sample</h2>
<p>Create a Blazor web app with no interactivity:</p>
<pre><code class="language-bash">dotnet new blazor -n StaticSsrState --interactivity None --empty
cd StaticSsrState
dotnet run
</code></pre>
<p>The sample is <a href="https://github.com/sumeyyeKurtulus/StaticSsrState">StaticSsrState</a>. The matrix in <a href="#behavior-to-check-on-the-sample">Behavior to check on the sample</a> is the contract from this article. It is not a filled-in record of a browser run.</p>
<p>Suggested layout:</p>
<ul>
<li><code>Program.cs</code> — session registration and TempData cookie policy</li>
<li><code>Components/Pages/Feedback.razor</code> — PRG flash</li>
<li><code>Components/Pages/Onboarding.razor</code> — three steps and a clear</li>
</ul>
<h3>Service registration</h3>
<p>Add the session services next to <code>AddRazorComponents</code>, and call <code>UseSession()</code> before the endpoint. Leave the rest of the generated pipeline in place.</p>
<pre><code class="language-csharp">builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =&gt;
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

builder.Services.AddRazorComponents(options =&gt;
{
    options.TempDataCookie.HttpOnly = true;
    options.TempDataCookie.SameSite = SameSiteMode.Lax;
    options.TempDataCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
</code></pre>
<pre><code class="language-csharp">app.UseSession();
app.MapRazorComponents&lt;App&gt;();
</code></pre>
<p>The sample does not call <code>AddInteractiveServerComponents()</code>. The two pages below are static on purpose. <code>UseAntiforgery()</code> is left out, matching the .NET 11 template. TempData's <code>SameSite</code> and <code>SecurePolicy</code> in this block are the defaults (<code>Lax</code>, <code>SameAsRequest</code>). The session cookie's <code>SecurePolicy</code> default is <code>None</code>; the sample sets <code>SameAsRequest</code> instead.</p>
<h3>Post-Redirect-Get flash message</h3>
<p><code>Feedback.razor</code> posts a comment, stores a one-line result in TempData, and redirects to itself. Enhanced form handling is off unless the form sets <code>Enhance</code> or <code>data-enhance</code>. <code>Enhance=&quot;false&quot;</code> changes nothing, so these forms omit it. The POST is a full document post. <code>NavigateTo(..., forceLoad: true)</code> loads the redirect target as a full document.</p>
<p>The handler writes <code>FlashMessage</code>. <code>OnInitialized</code> copies it into <code>_notice</code> and clears the parameter so the end-of-request write-back does not save the banner for the refresh after that.</p>
<pre><code class="language-razor">@page &quot;/feedback&quot;
@using System.ComponentModel.DataAnnotations
@inject NavigationManager Navigation

&lt;PageTitle&gt;Feedback&lt;/PageTitle&gt;

@if (!string.IsNullOrEmpty(_notice))
{
    &lt;p role=&quot;status&quot;&gt;@_notice&lt;/p&gt;
}

&lt;EditForm Model=&quot;Input&quot; FormName=&quot;feedback&quot; OnValidSubmit=&quot;Submit&quot;&gt;
    &lt;DataAnnotationsValidator /&gt;
    &lt;div&gt;
        &lt;label&gt;
            Comment
            &lt;InputText @bind-Value=&quot;Input.Comment&quot; /&gt;
        &lt;/label&gt;
        &lt;ValidationMessage For=&quot;() =&gt; Input.Comment&quot; /&gt;
    &lt;/div&gt;
    &lt;button type=&quot;submit&quot;&gt;Send&lt;/button&gt;
&lt;/EditForm&gt;

@code {
    private string? _notice;

    [SupplyParameterFromTempData(Name = &quot;flash_message&quot;)]
    public string? FlashMessage { get; set; }

    [SupplyParameterFromForm]
    public FeedbackInput Input { get; set; } = new();

    protected override void OnInitialized()
    {
        _notice = FlashMessage;
        FlashMessage = null;
    }

    private void Submit()
    {
        FlashMessage = $&quot;Thanks. We received \&quot;{Input.Comment}\&quot;.&quot;;
        Navigation.NavigateTo(&quot;/feedback&quot;, forceLoad: true);
    }

    public sealed class FeedbackInput
    {
        [Required]
        [StringLength(280)]
        public string? Comment { get; set; }
    }
}
</code></pre>
<p><a href="https://github.com/abpframework/abp/blob/c9a3ff76090326ab491d65082ecde1be5b03df44/docs/en/Community-Articles/2026-10-02-State-Without-a-Circuit%3A-TempData-and-Session-in-Blazor-.NET-11-Static-SSR/images/feedback.gif">Flash Message</a></p>
<p><code>[Required]</code> keeps an empty submit off the success path, so there is no fake error banner to invent. If you also want an error that survives a redirect, a failure that happens after validation, such as a downstream reject, set <code>FlashMessage</code> to that text and redirect the same way.</p>
<p>When one redirect is not enough, drop the attribute for that key and use the cascading dictionary:</p>
<pre><code class="language-csharp">protected override void OnInitialized()
{
    _notice = TempData?.Get(&quot;flash_message&quot;) as string;
    TempData?.Keep(&quot;flash_message&quot;);
}
</code></pre>
<p><code>Keep</code> is the exception. The feedback page should consume the message.</p>
<h3>Multi-step onboarding in session</h3>
<p><code>OnboardingDraft</code> is not a supported session type. The sample stores three values, and <code>null</code> removes each key. Finish assigns <code>Name</code>, <code>Company</code>, and <code>Step</code> to <code>null</code>. The next GET is written to render step 1.</p>
<pre><code class="language-razor">@page &quot;/onboarding&quot;
@using System.ComponentModel.DataAnnotations
@inject NavigationManager Navigation

&lt;PageTitle&gt;Onboarding&lt;/PageTitle&gt;

@if (Step is null || Step &lt;= 1)
{
    &lt;h1&gt;Your name&lt;/h1&gt;
    &lt;EditForm Model=&quot;NameInput&quot; FormName=&quot;onboarding-name&quot; OnValidSubmit=&quot;SaveName&quot;&gt;
        &lt;DataAnnotationsValidator /&gt;
        &lt;label&gt;
            Name
            &lt;InputText @bind-Value=&quot;NameInput.Name&quot; /&gt;
        &lt;/label&gt;
        &lt;ValidationMessage For=&quot;() =&gt; NameInput.Name&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Continue&lt;/button&gt;
    &lt;/EditForm&gt;
}
else if (Step == 2)
{
    &lt;h1&gt;Your company&lt;/h1&gt;
    &lt;p&gt;Name on file: @Name&lt;/p&gt;
    &lt;EditForm Model=&quot;CompanyInput&quot; FormName=&quot;onboarding-company&quot; OnValidSubmit=&quot;SaveCompany&quot;&gt;
        &lt;DataAnnotationsValidator /&gt;
        &lt;label&gt;
            Company
            &lt;InputText @bind-Value=&quot;CompanyInput.Company&quot; /&gt;
        &lt;/label&gt;
        &lt;ValidationMessage For=&quot;() =&gt; CompanyInput.Company&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Continue&lt;/button&gt;
    &lt;/EditForm&gt;
}
else
{
    &lt;h1&gt;Review&lt;/h1&gt;
    &lt;p&gt;@Name, @Company&lt;/p&gt;
    &lt;form method=&quot;post&quot; @formname=&quot;onboarding-finish&quot; @onsubmit=&quot;Finish&quot;&gt;
        &lt;AntiforgeryToken /&gt;
        &lt;button type=&quot;submit&quot;&gt;Finish and clear&lt;/button&gt;
    &lt;/form&gt;
}

@code {
    [SupplyParameterFromSession(Name = &quot;onboarding_name&quot;)]
    public string? Name { get; set; }

    [SupplyParameterFromSession(Name = &quot;onboarding_company&quot;)]
    public string? Company { get; set; }

    [SupplyParameterFromSession(Name = &quot;onboarding_step&quot;)]
    public int? Step { get; set; }

    [SupplyParameterFromForm]
    public NameInputModel NameInput { get; set; } = new();

    [SupplyParameterFromForm]
    public CompanyInputModel CompanyInput { get; set; } = new();

    protected override void OnInitialized()
    {
        NameInput ??= new();
        CompanyInput ??= new();
        NameInput.Name ??= Name;
        CompanyInput.Company ??= Company;
    }

    private void SaveName()
    {
        Name = NameInput.Name;
        Step = 2;
        Navigation.NavigateTo(&quot;/onboarding&quot;, forceLoad: true);
    }

    private void SaveCompany()
    {
        Company = CompanyInput.Company;
        Step = 3;
        Navigation.NavigateTo(&quot;/onboarding&quot;, forceLoad: true);
    }

    private void Finish()
    {
        Name = null;
        Company = null;
        Step = null;
        Navigation.NavigateTo(&quot;/onboarding&quot;, forceLoad: true);
    }

    public sealed class NameInputModel
    {
        [Required]
        public string? Name { get; set; }
    }

    public sealed class CompanyInputModel
    {
        [Required]
        public string? Company { get; set; }
    }
}
</code></pre>
<p><a href="https://github.com/abpframework/abp/blob/c9a3ff76090326ab491d65082ecde1be5b03df44/docs/en/Community-Articles/2026-10-02-State-Without-a-Circuit%3A-TempData-and-Session-in-Blazor-.NET-11-Static-SSR/images/onboarding.gif">Session Logic</a></p>
<p>Idle expiry is the other clear. Set <code>IdleTimeout</code> to 30 seconds in Development, wait, and reload. The three properties come back unset, and the page has to show step 1. Treat a missing step as a new draft, never as step 3 with blank fields. The <code>Step &lt;= 1</code> branch above is that guard. The sample leaves the timeout at 20 minutes.</p>
<p>A cart uses a supported collection, such as <code>List&lt;string&gt;</code> of product ids. <code>List&lt;CartLine&gt;</code> is rejected the same way <code>OnboardingDraft</code> was.</p>
<p>An interactive render mode does not supply these parameters. The property stays at its default, so a circuit is the wrong place to read the flash or the wizard. That limit is stated in <a href="#when-static-ssr-is-the-wrong-tool">When static SSR is the wrong tool</a>. The sample does not add a page for it.</p>
<h2>Security, scale-out, and the refresh button</h2>
<h3>Security and privacy</h3>
<p>Data Protection keys have to be the same on every node. A TempData cookie written on node A does not decrypt on node B when each machine has its own key ring. Share the key ring the same way you already share authentication-cookie keys: a file share, Redis, or a vault.</p>
<p>The cookie is encrypted and <code>HttpOnly</code>. It still travels on every request and sits in the browser profile. Keep secrets, tokens, and personal data out of it. Session stores the payload on the server and sends the session id. That is the right default as soon as the value identifies a person or grows past a short message.</p>
<p>Both forms still post an antiforgery token. Tampering with the TempData cookie should fail at decryption. Check that on the sample by editing the cookie and reloading: the page should come back with an empty flash and a log entry, not with attacker-controlled text in the paragraph.</p>
<h3>Scale-out</h3>
<p>In-memory distributed cache is one process. A second instance, or a recycle, drops every draft stored there. Session requires session affinity so the next request returns to the node that holds that session. Session-backed TempData has the same requirement. Cookie TempData needs shared Data Protection keys and does not need affinity.</p>
<h3>Refresh, Back, and copied links</h3>
<p>F5 after the feedback GET drops the banner. That is the lifetime you asked for when you chose TempData. F5 on the wizard keeps the draft.</p>
<p>Back and Forward move through URLs. They do not rewind TempData or Session the way they rewind <code>?step=2</code>. If the product needs history, put the step in the query string and keep only the draft body in session.</p>
<p>A copied <code>/onboarding</code> link opens step 1 on another computer, because the draft is in the first browser's session cookie. Publish a link only for state that lives in the URL or in a database row the recipient is allowed to read.</p>
<h3>When static SSR is the wrong tool</h3>
<p>Use Interactive Server or WebAssembly when the interface itself has to stay alive between requests: a drag-and-drop board, a circuit-scoped service, a component that mutates on <code>onclick</code> without a POST. <code>[PersistentState]</code> and <code>PersistentComponentState</code> carry prerendered values into that interactive runtime, and circuit persistence can resume an Interactive Server circuit after a disconnect. They are the wrong tool for a flash message after a redirect.</p>
<p>A practical split is a static page for the wizard and the banner, and an interactive render mode on the one component that needs a live event. Turning the whole app interactive in order to remember one string gives you a circuit per user and still loses the value on a full reload unless you persist it anyway.</p>
<h2>Behavior to check on the sample</h2>
<p>These rows are the contract from the sections above. They are not measured results. The last column is what to look for. If the browser disagrees, change this article.</p>
<p>| Action                                       | TempData contract                                                  | Session contract                        | What to look for                       |
| -------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------- | -------------------------------------- |
| Submit the feedback form                     | <code>flash_message</code> is stored, then read on the redirect GET           | —                                       | Banner text after redirect             |
| Submit onboarding step 1, then step 2        | —                                                                  | Name and step retained across both GETs | Name still visible on the company step |
| F5                                           | Consumed by the previous GET, and cleared by the <code>null</code> write-back | Name still present                      | Banner gone; name still there          |
| Open the same URL in a new tab               | Flash already consumed                                             | Same <code>.AspNetCore.Session</code> cookie       | Banner absent; draft visible           |
| Copy the URL into a private window           | No session cookie                                                  | No session cookie                       | Both pages empty                       |
| Finish onboarding                            | —                                                                  | Name, company, and step set to <code>null</code>   | Next GET is step 1                     |
| Idle timeout (set 30 seconds in Development) | —                                                                  | Session abandoned                       | Step 1, empty fields                   |
| Edit the TempData cookie and reload          | Decrypt fails, value becomes null                                  | —                                       | Empty banner, no injected markup       |</p>
<p>Also note, if you hit them: a log line when two components share one session key, cookie chunking if you stuff the feedback string toward 4 KB, and <code>Set-Cookie: .AspNetCore.Session</code> on a streaming response before the body. Cookie-backed TempData on that same streaming response cannot be saved.</p>
<p>Constraints that are already part of the contract, whether or not the demo surprises us:</p>
<ul>
<li>Both attributes are static SSR only.</li>
<li>Both attributes accept the closed list: <code>string</code>, <code>int</code>, <code>bool</code>, <code>Guid</code>, <code>DateTime</code>, int-backed enums, their nullables, <code>T[]</code>, <code>List&lt;T&gt;</code>, <code>HashSet&lt;T&gt;</code>, <code>Collection&lt;T&gt;</code>, <code>Dictionary&lt;string, T&gt;</code>, and <code>object[]</code>. A custom class is rejected. The wizard stores <code>string?</code> and <code>int?</code>.</li>
<li>Cookie TempData is a short, non-sensitive message. Large TempData, and any TempData on a streaming response, moves to the session provider and requires session affinity.</li>
<li>Session does nothing until <code>AddSession</code> and <code>UseSession</code> are both there. A load-balanced deployment also needs session affinity.</li>
<li>One TempData provider is active.</li>
</ul>
<h2>Adoption checklist</h2>
<ul>
<li>Flash after one redirect → TempData. Several requests, and the value must stay out of the URL → Session. A link someone else can open → query string. Still there after the browser closes → database.</li>
<li>The page is static SSR. An interactive render mode will not populate these parameters.</li>
<li><code>AddRazorComponents</code> is registered. Session also has a distributed cache, <code>AddSession</code>, and <code>UseSession</code> before the endpoint.</li>
<li>The flash sample copies the TempData value out and assigns <code>null</code>, so write-back does not keep the banner.</li>
<li>TempData holds no secrets and no personal data. Session holds no unbounded blobs.</li>
<li>Data Protection keys are shared across the farm. Session and session-backed TempData run with session affinity.</li>
<li>Production cookies set <code>Secure</code>. Session is <code>IsEssential</code> only when the flow actually requires the cookie.</li>
<li>Form posts still include the antiforgery token. <code>UseAntiforgery()</code> is optional in .NET 11 and is not in the template.</li>
<li>Finish, cancel, and idle expiry all end on the empty state.</li>
<li>F5, a new tab, and a copied link were tried, and what you saw matches the &quot;what to look for&quot; column. Until that happens, the matrix stays unchecked.</li>
<li>Attribute names and cookie defaults were rechecked on the SDK you ship.</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11">What's new in ASP.NET Core in .NET 11</a> — TempData, <code>[SupplyParameterFromSession]</code>, and the streaming-SSR session-cookie fix. This article tracks <strong>.NET 11 RC1</strong></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/server">ASP.NET Core Blazor server-side state management</a> — <code>ITempData</code>, both attributes, cookie and session option tables</li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/navigation#enhanced-navigation-and-form-handling">Enhanced navigation and form handling</a> — <code>Enhance</code> and <code>data-enhance</code> are opt-in</li>
<li>dotnet/aspnetcore: PR <a href="https://github.com/dotnet/aspnetcore/pull/64749">#64749</a> (TempData for Blazor SSR), PR <a href="https://github.com/dotnet/aspnetcore/pull/65306">#65306</a> (<code>[SupplyParameterFromTempData]</code>), PR <a href="https://github.com/dotnet/aspnetcore/pull/65184">#65184</a> (<code>[SupplyParameterFromSession]</code>), PR <a href="https://github.com/dotnet/aspnetcore/pull/66832">#66832</a> (streaming SSR session cookie and TempData persistence)</li>
<li><a href="https://abp.io/community/articles/quickgrid-in-.net-11-sorting-and-paging-that-live-in-the-url-hypzmikw#gsc.tab=0">QuickGrid in .NET 11: Sorting and Paging That Live in the URL</a> — when the query string is the better store</li>
<li><a href="https://github.com/sumeyyeKurtulus/StaticSsrState">Demo</a> <code>StaticSsrState</code></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a240eb1-0295-587e-3bfa-0ac3b429b7d4" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a240eb1-0295-587e-3bfa-0ac3b429b7d4" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/angular-resource-apis-explained-generating-signalbased-service-proxies-ksup6l1n</guid>
      <link>https://abp.io/community/posts/angular-resource-apis-explained-generating-signalbased-service-proxies-ksup6l1n</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>state-management</category>
      <category>client-proxy</category>
      <category>abp</category>
      <category>code-generator</category>
      <title>Angular Resource APIs Explained: Generating Signal-Based Service Proxies</title>
      <description>Angular Resource APIs introduce a declarative, Signal-driven approach to data fetching, eliminating much of the boilerplate traditionally associated with asynchronous state management. In this article, we'll explore Angular's Resource APIs, compare them with Observable-based patterns, and see how ABP integrates them into generated service APIs. You'll learn when to use Resource APIs, how they work under the hood, and how they help you build cleaner, more reactive Angular applications.</description>
      <pubDate>Mon, 14 Sep 2026 08:14:38 Z</pubDate>
      <a10:updated>2026-10-05T09:29:39Z</a10:updated>
      <content:encoded><![CDATA[<h1>Angular Resource APIs Explained: Generating Signal-Based Service Proxies</h1>
<p>Angular's shift toward <strong>Signals</strong> has changed how applications model state and data loading. Instead of manually coordinating subscriptions, loading flags, and error handling, Angular now provides resource APIs that integrate asynchronous data fetching directly into the signal ecosystem.</p>
<p>To embrace this programming model, ABP now offers an <strong>optional Resource API generation mode</strong> for Angular service proxies. When enabled, the proxy generator produces signal-friendly APIs for read operations while preserving the familiar Observable-based experience for the rest of your application.</p>
<p>This article introduces the new Resource API generation mode, explains why it exists, how it works, and when it should be preferred over traditional Observable-based proxies.</p>
<hr />
<h2>Why Resource APIs</h2>
<p>For years, Angular applications have relied on observable-based services to communicate with backend APIs. While Observables remain extremely powerful, consuming them inside components often requires additional state management.</p>
<p>A typical read operation usually involves:</p>
<ul>
<li>subscribing to an Observable,</li>
<li>tracking loading state,</li>
<li>storing the latest value,</li>
<li>handling errors,</li>
<li>cleaning up subscriptions when necessary.</li>
</ul>
<p>As applications adopt Signals for local state, this pattern starts to feel increasingly imperative. The component becomes responsible not only for displaying data, but also for orchestrating the entire request lifecycle.</p>
<p>Angular's Resource APIs address this problem by treating asynchronous data as reactive state. Instead of manually reacting to changes, you describe <strong>what the request depends on</strong>, and Angular automatically keeps the resource synchronized.</p>
<p>For example, if a request depends on an entity ID represented as a <code>Signal</code>, changing that signal automatically triggers a new request. The component no longer needs to manually subscribe or invoke refresh logic simply because an input changed.</p>
<p>This approach offers several benefits:</p>
<ul>
<li>loading, error, and value state are managed together,</li>
<li>requests automatically react to signal changes,</li>
<li>components become more declarative,</li>
<li>significantly less boilerplate is required.</li>
</ul>
<p>Resource APIs are therefore an excellent fit for <strong>read-only data retrieval</strong>, where the primary goal is to keep the UI synchronized with backend state.</p>
<hr />
<h2>What Angular Gives Us</h2>
<p>Angular provides several APIs for working with asynchronous resources, each targeting a slightly different use case.</p>
<h3><code>resource</code></h3>
<p>The <code>resource</code> API is Angular's generic primitive for asynchronous state.</p>
<p>It is designed for loaders that return promise-based results and automatically exposes:</p>
<ul>
<li>the current value,</li>
<li>loading status,</li>
<li>errors,</li>
<li>reload capabilities.</li>
</ul>
<p>This API is ideal when your asynchronous source is not based on RxJS.</p>
<h3><code>rxResource</code></h3>
<p>Most Angular applications—including ABP applications—already communicate with the backend through <code>HttpClient</code>, whose APIs return Observables.</p>
<p>The <code>rxResource</code> helper bridges these existing Observable streams with Angular's Resource model. Instead of rewriting existing services to use Promises, it simply wraps an Observable-producing function and exposes it as a reactive resource.</p>
<p>Internally, the new ABP proxy generation mode uses this approach. Generated resource methods eventually call the existing <code>RestService</code> request pipeline, while exposing the result as an Angular <code>ResourceRef</code>. This means existing authentication, interceptors, multi-tenancy, localization, error handling, and other ABP infrastructure continue to work exactly as before.</p>
<h3><code>httpResource</code></h3>
<p>Angular also provides <code>httpResource</code>, which is a specialized resource implementation built directly on top of <code>HttpClient</code>.</p>
<p>For applications making direct HTTP requests, this can be a convenient option.</p>
<p>However, ABP applications already centralize HTTP communication through <code>RestService</code>, which adds framework-specific behavior around every request. Because of that, the generated proxies rely on <code>rxResource</code> rather than <code>httpResource</code>, allowing them to preserve the entire ABP request pipeline while still providing a signal-based API.</p>
<p>In short:</p>
<p>| API | Intended for |
| --- | --- |
| <code>resource</code> | Generic asynchronous loaders returning Promises |
| <code>rxResource</code> | Existing RxJS/Observable-based data sources |
| <code>httpResource</code> | Direct <code>HttpClient</code> requests without additional abstraction |</p>
<p>Since ABP proxies already build on top of <code>RestService</code>, <code>rxResource</code> is the natural choice for bringing Signal-based data loading to generated client proxies.</p>
<hr />
<h2>Why This Feature Is Opt-In</h2>
<p>One of the primary goals of the new Resource API generation mode is to introduce a modern, Signal-friendly API <strong>without disrupting existing applications</strong>.</p>
<p>ABP's generated Angular service proxies have been Observable-based for years, and they continue to serve a wide range of applications effectively. Many projects already rely on RxJS operators, custom Observable pipelines, and existing component patterns. Replacing those generated APIs would introduce unnecessary breaking changes for little benefit.</p>
<p>Instead, Resource API generation is <strong>completely opt-in</strong>.</p>
<p>By passing the <code>--resource-api</code> option to the <code>abp generate-proxy</code> command, the generator produces additional resource helpers designed for Angular's Signal ecosystem. If you do not enable the option, proxy generation behaves exactly as it always has, producing the familiar Observable-based services.</p>
<p>This gradual approach offers several advantages:</p>
<ul>
<li>Existing applications continue working without modification.</li>
<li>Teams can adopt Signals incrementally instead of performing a large migration.</li>
<li>New pages can embrace Resource APIs while older features continue using Observables.</li>
<li>Developers remain free to choose the programming model that best fits each feature.</li>
</ul>
<p>Another intentional design decision is that <strong>only read operations receive Resource API helpers</strong>.</p>
<p>Resource APIs naturally model asynchronous state that can be refreshed whenever their reactive inputs change. This makes them an excellent fit for <code>GET</code> requests, where the goal is to retrieve and synchronize data.</p>
<p>Write operations such as <code>POST</code>, <code>PUT</code>, <code>PATCH</code>, and <code>DELETE</code> represent user actions rather than continuously synchronized state. These operations are typically composed with RxJS pipelines, optimistic updates, notifications, or custom error handling, making Observables the more appropriate abstraction.</p>
<p>By limiting Resource APIs to read operations, generated proxies follow Angular's recommended usage patterns while keeping mutation APIs predictable and familiar.</p>
<hr />
<h2>Generator Behavior</h2>
<p>Enabling Resource API generation requires only a single additional option:</p>
<pre><code class="language-bash">abp generate-proxy -t ng --resource-api
</code></pre>
<p>Without this option, the generator behaves exactly as before, producing standard Observable-based service methods.</p>
<p>For example, a generated service might expose a method like:</p>
<pre><code class="language-tsx">bookService.getList(input);
</code></pre>
<p>which returns an <code>Observable</code> and can be consumed with RxJS operators or converted into Signals using Angular's interoperability utilities.</p>
<p>When <code>--resource-api</code> is enabled, the generator <strong>preserves these existing methods</strong> and additionally generates companion Resource API methods for every <code>GET</code> endpoint.</p>
<p>For example:</p>
<pre><code class="language-tsx">bookService.getList(...);          // Observable
bookService.getListResource(...);  // ResourceRef
</code></pre>
<p>These generated resource methods internally use Angular's <code>rxResource</code> while continuing to execute requests through ABP's existing <code>RestService</code>. As a result, the complete ABP request pipeline—including authentication, interceptors, localization, multi-tenancy, and error handling—remains unchanged.</p>
<p>The generator only creates resource helpers for <code>GET</code> endpoints. Mutation methods such as <code>create</code>, <code>update</code>, and <code>delete</code> continue to return Observables exactly as before.</p>
<p>This selective generation keeps the generated API straightforward:</p>
<ul>
<li><strong>Read operations</strong> gain Signal-friendly Resource APIs.</li>
<li><strong>Write operations</strong> continue using Observables.</li>
<li><strong>Existing Observable methods remain available</strong>, allowing gradual adoption without forcing a migration.</li>
</ul>
<p>In the next section, we'll examine the shape of the generated Resource API methods and see how they integrate naturally with Angular Signals.</p>
<hr />
<h2>Generated API Shape</h2>
<p>The generated Resource API methods closely resemble their Observable counterparts, making them easy to adopt. Rather than introducing an entirely new programming model, they adapt the existing proxy APIs to Angular's Signal ecosystem.</p>
<p>There are, however, a few important differences.</p>
<h3>Signal-Based Inputs</h3>
<p>Traditional proxy methods accept plain values:</p>
<pre><code class="language-tsx">bookService.get(id);
</code></pre>
<p>The generated Resource API methods instead accept <strong>Signals</strong>.</p>
<pre><code class="language-tsx">const id = signal('42');

const book = bookService.getResource(id);
</code></pre>
<p>Because the input is reactive, Angular automatically re-executes the request whenever the signal value changes.</p>
<pre><code class="language-tsx">id.set('43');
</code></pre>
<p>No additional method calls, subscriptions, or refresh logic are required—the resource stays synchronized with its dependencies automatically.</p>
<h3>Returning a <code>ResourceRef</code></h3>
<p>Observable-based proxy methods return an <code>Observable&lt;T&gt;</code>.</p>
<p>Resource methods instead return a <code>ResourceRef&lt;T&gt;</code>, which exposes the request state as Signals.</p>
<p>This gives components access to everything they typically need during data loading:</p>
<ul>
<li>the current value,</li>
<li>loading status,</li>
<li>any request error,</li>
<li>reload functionality.</li>
</ul>
<p>A component can consume the resource directly:</p>
<pre><code class="language-tsx">const book = bookService.getResource(id);

book.value();
book.isLoading();
book.error();
book.reload();
</code></pre>
<p>Instead of maintaining separate signals for loading, data, and errors, the resource keeps these states together in a single reactive object.</p>
<h3>Reactive Request Construction</h3>
<p>Many API endpoints require multiple parameters or request objects.</p>
<p>Rather than asking developers to manually rebuild those objects whenever an input changes, the generated methods construct the request reactively using Angular's <code>computed()</code> API.</p>
<p>Suppose a request depends on pagination and a search keyword:</p>
<pre><code class="language-tsx">const page = signal(1);
const filter = signal('');
</code></pre>
<p>The generated proxy internally derives the request object from these Signals.</p>
<p>Whenever either value changes, Angular recomputes the request and automatically performs a new HTTP request.</p>
<p>This means developers only manage application state. The generated proxy takes care of determining <strong>when</strong> a request should be refreshed.</p>
<hr />
<h2>Examples</h2>
<p>Let's compare a few common scenarios to see how generated Resource APIs look in practice.</p>
<h3>Fetching a Single Entity</h3>
<p>A traditional generated proxy might be used like this:</p>
<pre><code class="language-tsx">bookService.get(id).subscribe(...);
</code></pre>
<p>With Resource API generation enabled:</p>
<pre><code class="language-tsx">const id = signal('42');

const book = bookService.getResource(id);
</code></pre>
<p>Changing the ID automatically reloads the resource.</p>
<pre><code class="language-tsx">id.set('84');
</code></pre>
<p>The component simply reads the latest value:</p>
<pre><code class="language-tsx">book.value();
</code></pre>
<p>Angular handles the request lifecycle automatically.</p>
<h3>Fetching a List with Query Parameters</h3>
<p>List endpoints become even more interesting because they usually depend on multiple reactive inputs.</p>
<p>Consider a search page with pagination.</p>
<pre><code class="language-tsx">const filter = signal('');
const skipCount = signal(0);
const maxResultCount = signal(10);
</code></pre>
<p>Using the generated proxy is straightforward:</p>
<pre><code class="language-tsx">const books = bookService.getListResource({
  filter,
  skipCount,
  maxResultCount,
});
</code></pre>
<p>Whenever any of these Signals changes, Angular automatically refreshes the resource.</p>
<pre><code class="language-tsx">filter.set('Angular');
skipCount.set(20);
</code></pre>
<p>There is no need to manually call <code>getList()</code> again or synchronize subscriptions with UI state.</p>
<h3>Behind the Scenes</h3>
<p>Although the generated API feels simple to consume, the proxy performs a considerable amount of work internally.</p>
<p>Conceptually, the generated method looks similar to the following:</p>
<pre><code class="language-tsx">getListResource(input: {
  filter: Signal&lt;string&gt;;
  skipCount: Signal&lt;number&gt;;
  maxResultCount: Signal&lt;number&gt;;
}): ResourceRef&lt;PagedResultDto&lt;BookDto&gt;&gt; {
  return this.restService.requestResource({
    method: 'GET',
    url: '/api/app/books',
    params: computed(() =&gt; ({
      filter: input.filter(),
      skipCount: input.skipCount(),
      maxResultCount: input.maxResultCount(),
    })),
  });
}
</code></pre>
<p>Notice that every request parameter is read inside a <code>computed()</code> callback. This establishes Angular's reactive dependency tracking, ensuring that any change to an input Signal automatically triggers a new request.</p>
<p>As a consumer, you never need to think about this implementation detail. You simply update your Signals, and the generated proxy keeps the resource synchronized with your application's state.</p>
<hr />
<h2>Best Practices</h2>
<p>As with any new Angular feature, Resource APIs are most effective when used in the scenarios they were designed for. The following recommendations can help you get the most out of generated Resource-based proxies while keeping your codebase clean and maintainable.</p>
<h3>Use Resource APIs for Read Operations</h3>
<p>Resource APIs are designed to represent <strong>reactive, read-only state</strong>.</p>
<p>Whenever your UI needs to retrieve data that should stay synchronized with one or more Signals, generated resource methods are an excellent choice.</p>
<p>Typical examples include:</p>
<ul>
<li>entity details,</li>
<li>paginated lists,</li>
<li>search results,</li>
<li>dashboard widgets,</li>
<li>lookup data.</li>
</ul>
<p>These scenarios naturally benefit from automatic reloading whenever their reactive inputs change.</p>
<h3>Keep Mutations as Observable Methods</h3>
<p>Operations that modify server state—such as creating, updating, or deleting data—should continue using the standard Observable-based proxy methods.</p>
<p>Mutation requests often involve additional workflows such as:</p>
<ul>
<li>displaying success notifications,</li>
<li>handling validation errors,</li>
<li>optimistic UI updates,</li>
<li>chaining additional requests,</li>
<li>navigation after completion.</li>
</ul>
<p>These workflows fit naturally into RxJS pipelines, which is why the generator intentionally keeps write operations unchanged.</p>
<h3>Let Signals Drive Your Requests</h3>
<p>One of the biggest advantages of Resource APIs is that you no longer decide <strong>when</strong> to issue a request.</p>
<p>Instead, you describe <strong>what the request depends on</strong>, and Angular takes care of the rest.</p>
<p>Rather than writing code like:</p>
<pre><code class="language-tsx">filter.valueChanges.subscribe(() =&gt; loadBooks());
</code></pre>
<p>simply update the underlying Signals.</p>
<p>Whenever those Signals change, the generated resource automatically refreshes itself.</p>
<p>This declarative approach removes much of the manual subscription and synchronization logic that traditionally accumulates in Angular components.</p>
<h3>Build Request Objects with <code>computed()</code></h3>
<p>When an endpoint accepts multiple parameters, derive the request object using <code>computed()</code> instead of constructing it manually.</p>
<p>For example:</p>
<pre><code class="language-tsx">const request = computed(() =&gt; ({
  filter: filter(),
  skipCount: skipCount(),
  maxResultCount: maxResultCount(),
}));
</code></pre>
<p>This ensures Angular can correctly track every dependency involved in the request.</p>
<p>Likewise, avoid reading Signal values outside of the reactive computation.</p>
<p>Instead of extracting Signal values first:</p>
<pre><code class="language-tsx">const currentFilter = filter();

const request = computed(() =&gt; ({
  filter: currentFilter,
}));
</code></pre>
<p>read them directly inside the <code>computed()</code> callback:</p>
<pre><code class="language-tsx">const request = computed(() =&gt; ({
  filter: filter(),
}));
</code></pre>
<p>Doing so allows Angular to react whenever those Signals change.</p>
<h3>Consume the Entire <code>ResourceRef</code></h3>
<p>A generated resource provides much more than the retrieved value.</p>
<p>Instead of creating separate Signals for loading and error state, consume the <code>ResourceRef</code> directly.</p>
<p>For example:</p>
<ul>
<li><code>value()</code> exposes the latest successful result.</li>
<li><code>isLoading()</code> indicates whether a request is in progress.</li>
<li><code>error()</code> provides the latest request failure.</li>
<li><code>reload()</code> manually refreshes the resource when needed.</li>
</ul>
<p>Keeping these states together makes components easier to understand and reduces state duplication.</p>
<h3>Choose the Right Resource API</h3>
<p>Angular offers multiple Resource APIs, each serving a different purpose.</p>
<p>For generated ABP service proxies, the recommended approach is the generated Resource API based on <code>rxResource</code>, since it preserves the existing <code>RestService</code> pipeline and all of its framework integrations.</p>
<p>If you're writing standalone Angular services that communicate directly with <code>HttpClient</code>, <code>httpResource</code> may be a better fit.</p>
<p>Choosing the appropriate abstraction helps keep both your generated code and handwritten services consistent with Angular's intended usage patterns.</p>
<hr />
<h2>Conclusion</h2>
<p>Angular Signals have introduced a more declarative way to manage application state, and Resource APIs naturally extend that model to asynchronous data loading.</p>
<p>With Resource API generation, ABP brings this programming model directly into generated Angular service proxies. Read operations become reactive, Signal-driven, and significantly easier to consume, while write operations continue using the familiar Observable-based APIs that already work well for mutations.</p>
<p>Most importantly, this evolution is completely opt-in. Existing applications can continue using their current proxies without modification, while new features can gradually adopt Resource APIs wherever they provide the greatest benefit.</p>
<p>Whether you're building a new Signal-first Angular application or incrementally modernizing an existing ABP solution, Resource-based proxy generation offers a straightforward path toward cleaner components, less boilerplate, and a more reactive data-fetching experience.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a23b128-5b83-cd98-3379-6fd82113126c" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a23b128-5b83-cd98-3379-6fd82113126c" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw</guid>
      <link>https://abp.io/community/posts/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>state-management</category>
      <category>best-practices</category>
      <category>typescript</category>
      <category>frontend</category>
      <title>Angular 22 State Management: Signals, SignalStore, or NgRx?</title>
      <description>Angular 22 introduces a signal-first approach to building reactive applications. This guide explores how Signals, SignalStore, Resources, and Signal Forms fit together, when to choose each solution, and the best practices for creating scalable, maintainable Angular applications with NgRx.</description>
      <pubDate>Tue, 30 Jun 2026 07:38:01 Z</pubDate>
      <a10:updated>2026-10-05T09:06:37Z</a10:updated>
      <content:encoded><![CDATA[<h1>Angular 22 State Management: Signals, SignalStore, or NgRx?</h1>
<p>Angular has been steadily moving toward a signal-first architecture since the introduction of Signals in Angular 16. With Angular 22, that transition reaches another milestone. Signals are now at the center of Angular's reactive programming model, while APIs such as Resource and Signal Forms have matured into production-ready solutions. Combined with the framework's continued investment in zoneless change detection, these improvements significantly influence how Angular applications should manage state.</p>
<p>This shift also changes the role of NgRx. While the classic NgRx Store remains a powerful solution for large, event-driven applications, many scenarios that previously required reducers, selectors, and effects can now be implemented with much simpler, feature-scoped signal stores. Rather than replacing NgRx, Angular 22 encourages developers to choose the right state management strategy based on the scope and complexity of the problem.</p>
<p>In this article, we'll explore how Angular 22 changes the state management landscape, compare the classic NgRx Store with NgRx SignalStore, and demonstrate best practices for building modern Angular applications. We'll also discuss how Angular's new reactive APIs fit into enterprise applications and what these changes mean for projects built with the ABP Framework.</p>
<h2>Why Angular 22 Changes State Management</h2>
<p>Angular Signals introduced a fundamentally different approach by providing fine-grained reactivity built directly into the framework. Instead of propagating changes through Observable streams, Signals allow Angular to track exactly which pieces of state are consumed and update only the affected parts of the UI. This results in more predictable rendering, less boilerplate, and improved runtime performance.</p>
<p>Angular 22 builds on this foundation by making Signals the preferred reactive primitive throughout the framework. New APIs such as <strong>Resource</strong> for asynchronous data loading and <strong>Signal Forms</strong> for reactive forms integrate naturally with Signals, reducing the need for custom RxJS pipelines in many common scenarios.</p>
<p>For developers using NgRx, this doesn't mean abandoning existing applications or rewriting every store. Instead, it changes how state management should be approached. Component-local state can often be managed with plain Signals, feature-level state fits naturally into SignalStore, and the classic NgRx Store continues to excel for large-scale applications that benefit from centralized event streams, auditing, and global state synchronization.</p>
<p>Understanding these changing responsibilities is the key to designing maintainable Angular applications in the Angular 22 era. A practical way to think about state management is to start with the simplest solution and introduce additional abstractions only when the application's complexity requires them.</p>
<h2>Use Signals for Local Component State</h2>
<p>Plain Angular Signals are ideal for state that belongs exclusively to a single component. Examples include dialog visibility, selected tabs, loading indicators, filter values, or temporary form data.</p>
<p>Signals provide a straightforward API with minimal overhead and integrate seamlessly with Angular's change detection. For state that never needs to be shared outside a component or its immediate children, introducing a dedicated store often adds unnecessary complexity.</p>
<p>A settings page often contains UI state that doesn't need to be shared with the rest of the application. Using a dedicated store for this would introduce unnecessary complexity.</p>
<pre><code class="language-ts">@Component({...})
export class UserListComponent {
  readonly search = signal('');
  readonly showInactive = signal(false);

  readonly filteredUsers = computed(() =&gt;
    this.users().filter(user =&gt;
      user.name.includes(this.search()) &amp;&amp;
      (this.showInactive() || user.active)
    )
  );
}
</code></pre>
<p>This state is entirely local to the component and doesn't justify introducing a SignalStore.</p>
<h2>Use NgRx SignalStore for Feature State</h2>
<p>As applications grow, state often needs to be shared across multiple components within the same feature. Examples include user profiles, shopping carts, administration screens, dashboards, or settings pages.</p>
<p>NgRx SignalStore is designed specifically for these scenarios. It combines Angular Signals with a lightweight, feature-oriented architecture where state, computed values, and business logic are defined together. Instead of scattering logic across reducers, selectors, effects, and services, developers can keep everything related to a feature inside a single store.</p>
<p>SignalStore also integrates naturally with Angular's signal-based APIs, making it an excellent choice for modern Angular applications built around Resources and Signal Forms.</p>
<p>A User Management module is shared by multiple pages. The selected user, filters, and loaded entities should remain synchronized across those pages.</p>
<pre><code class="language-ts">export const UserStore = signalStore(
  withState({
    users: [] as User[],
    selectedUserId: null as number | null,
    loading: false,
  }),

  withComputed(({ users, selectedUserId }) =&gt; ({
    selectedUser: computed(() =&gt;
      users().find(x =&gt; x.id === selectedUserId())
    ),
  })),

  withMethods((store) =&gt; ({
    selectUser(id: number) {
      patchState(store, { selectedUserId: id });
    },
  })),
);
</code></pre>
<p>Everything related to the feature lives in one place: state, derived values, and business operations.</p>
<h2>NgRx Store vs. NgRx SignalStore</h2>
<p>Although both solutions belong to the NgRx ecosystem, they are designed to solve different architectural problems.</p>
<p>The classic NgRx Store follows the Redux pattern, where every state change is represented by an action that flows through reducers before producing a new immutable state. This explicit, event-driven architecture provides excellent traceability and scales well for applications with extensive global interactions.</p>
<p>SignalStore takes a different approach. Instead of centering the application around dispatched actions, it treats state as a reactive service built with Angular Signals. A SignalStore typically contains three core building blocks:</p>
<ul>
<li><strong>State</strong>, which represents the application's reactive data.</li>
<li><strong>Computed signals</strong>, which derive values from existing state.</li>
<li><strong>Methods</strong>, which encapsulate business logic and state updates.</li>
</ul>
<p>This functional model significantly reduces boilerplate while remaining predictable and testable. Since it builds directly on Angular Signals, it also integrates naturally with Angular's fine-grained change detection without requiring selectors or <code>async</code> pipes for many common scenarios.</p>
<p>The following comparison summarizes the strengths of each approach.</p>
<p>| Feature          | Classic NgRx Store              | NgRx SignalStore                  |
| ---------------- | ------------------------------- | --------------------------------- |
| Architecture     | Redux-based global store        | Feature-oriented reactive store   |
| Reactivity       | RxJS Observables                | Angular Signals                   |
| Boilerplate      | Higher                          | Lower                             |
| State Scope      | Global application state        | Feature or route state            |
| Side Effects     | Effects                         | Store methods or <code>rxMethod</code>       |
| Change Detection | Observable subscriptions        | Native signal reactivity          |
| Best For         | Large event-driven applications | Modern feature-based applications |</p>
<p>For most new Angular 22 applications, SignalStore is an excellent default choice for feature-level state management because it embraces the framework's signal-first architecture while keeping code concise and maintainable. The classic NgRx Store remains indispensable for applications that rely heavily on centralized event processing, global synchronization, or advanced debugging capabilities.</p>
<p>Instead of asking <em>&quot;Which one should I use?&quot;</em>, the better question is <em>&quot;Which scope of state am I trying to manage?&quot;</em> The answer usually determines the appropriate solution.</p>
<h2>Angular 22 Features That Improve State Management</h2>
<p>Angular 22 introduces several framework APIs that naturally complement modern state management patterns. Rather than replacing NgRx, these APIs reduce the amount of custom infrastructure developers previously had to build around it.</p>
<h3>Resource API</h3>
<p>One of the most significant additions is the <strong>Resource API</strong>, which provides a signal-based approach to asynchronous data loading.</p>
<p>Historically, fetching remote data in Angular involved coordinating <code>HttpClient</code>, RxJS operators, subscriptions, loading flags, and error handling. While these patterns remain valid, they often require considerable boilerplate even for straightforward scenarios.</p>
<p>Resources encapsulate these concerns into a single reactive abstraction. A Resource automatically tracks the signals it depends on, performs requests when those dependencies change, cancels obsolete requests, and exposes its lifecycle through reactive state such as the current value, loading status, and errors.</p>
<p>This makes Resources particularly well suited for read-oriented operations where data should stay synchronized with application state.</p>
<p>For example, changing a selected user ID can automatically trigger a new request without manually wiring <code>switchMap</code> or managing subscription lifecycles.</p>
<pre><code class="language-tsx">const userResource = httpResource(() =&gt; ({
  url: `/api/users/${selectedUserId()}`
}));
</code></pre>
<h3>Signal Forms</h3>
<p>Another major improvement is the stabilization of <strong>Signal Forms</strong>.</p>
<p>Traditional Reactive Forms expose their state through <code>FormControl</code> and <code>FormGroup</code> instances, requiring developers to query validation status, dirty state, touched state, and values through an imperative API.</p>
<p>Signal Forms expose these properties as signals instead. Every field becomes reactive by default, making templates easier to read while eliminating much of the manual state synchronization commonly found in form-heavy applications.</p>
<pre><code class="language-html">@if (profileForm.email.invalid() &amp;&amp; profileForm.email.touched()) {
  &lt;span&gt;Please enter a valid email.&lt;/span&gt;
}
</code></pre>
<p>Because field state is already reactive, components rarely need additional subscriptions or helper observables to keep the UI synchronized.</p>
<p>It's important to note that Signal Forms are responsible for <strong>UI state</strong>, while business operations such as saving data, loading entities, or handling server responses still belong in a dedicated service or SignalStore. Keeping these responsibilities separate results in components that remain focused on presentation while stores continue to own application logic.</p>
<h2>Best Practices for Building Modern SignalStores</h2>
<p>SignalStore significantly reduces the ceremony traditionally associated with state management, but the same architectural principles still apply. A well-designed store should encapsulate business logic without becoming responsible for concerns that belong elsewhere.</p>
<ol>
<li>Keep Stores Focused on a Single Feature
A SignalStore should represent a cohesive business feature rather than becoming a global container for unrelated state.
For example, an administration module might expose separate stores for users, roles, and permissions instead of combining all administrative functionality into a single, monolithic store. Smaller stores are easier to test, understand, and maintain over time.</li>
<li>Store Business State, Not UI State
Not every piece of state belongs in a store.
Transient UI concerns such as dialog visibility, selected tabs, expanded panels, or temporary input values are usually better managed with plain Signals inside the component.
Stores should own state that represents the application's business domain—entities, filters, permissions, settings, or data shared across multiple components.</li>
<li>Derive State Instead of Duplicating It
Whenever possible, compute values instead of storing them.
SignalStore's <code>withComputed()</code> feature makes it easy to derive reactive values from existing state, reducing the likelihood of inconsistent or stale data.
Instead of storing both a list of users and an active user count, derive the count directly from the collection.</li>
</ol>
<pre><code class="language-tsx">withComputed(({ users }) =&gt; ({
  activeUsers: computed(() =&gt;
    users().filter(user =&gt; user.active).length
  ),
}))
</code></pre>
<p>Keeping a single source of truth simplifies updates and reduces maintenance.
4. Prefer Immutable State Updates
Although SignalStore simplifies updates through <code>patchState()</code>, state should still be treated as immutable.
Updating only the affected portions of state makes changes predictable and allows Angular's signal system to     efficiently notify dependent computations.</p>
<pre><code class="language-tsx">patchState(store, {
  users: [...store.users(), newUser]
});
</code></pre>
<h2>Integrating Resources with SignalStore</h2>
<p>Resources and SignalStore solve different problems, and understanding their responsibilities leads to a cleaner architecture.</p>
<p>A <strong>Resource</strong> is responsible for synchronizing data with a remote source. It knows how to load data, react to parameter changes, expose loading and error states, and keep requests up to date.</p>
<p>A <strong>SignalStore</strong>, on the other hand, owns the application's business state. It coordinates operations, exposes domain-specific methods, derives computed values, and serves as the single source of truth for a feature.</p>
<p>Rather than replacing one another, they work best together.</p>
<p>A common pattern is to use a Resource for loading entities while allowing the store to expose business operations that modify those entities.</p>
<pre><code class="language-tsx">export const UserStore = signalStore(
  withState({
    selectedUserId: undefined as number | undefined,
  }),

  withComputed(({ selectedUserId }) =&gt; ({
    userResource: httpResource&lt;User&gt;(() =&gt; {
      const id = selectedUserId();

      return id
        ? {
            url: `/api/users/${id}`,
          }
        : undefined;
    }),
  })),

  withMethods((store) =&gt; ({
    selectUser(id: number) {
      patchState(store, {
        selectedUserId: id,
      });
    },
  })),
);
</code></pre>
<p>In this example, changing the selected user automatically causes the Resource to fetch new data. The store doesn't need to manage subscriptions or manually coordinate loading indicators because the Resource already exposes this information through signals.</p>
<p>This separation keeps data synchronization declarative while allowing the store to remain focused on business behavior.</p>
<h2>Integrating Signal Forms with SignalStore</h2>
<p>Signal Forms and SignalStore naturally complement one another because both are built on Angular Signals. However, they should not be treated as interchangeable.</p>
<p>Signal Forms are responsible for managing user input and validation, while SignalStore coordinates business operations such as loading, updating, and persisting data.</p>
<p>A common workflow consists of four steps:</p>
<ol>
<li>Load the entity through the store.</li>
<li>Populate the Signal Form.</li>
<li>Allow the user to edit the data.</li>
<li>Submit the updated values back to the store.</li>
</ol>
<p>The component remains responsible only for orchestrating the interaction between the form and the store.</p>
<pre><code class="language-tsx">@Component({
  // ...
})
export class UserEditorComponent {
  readonly store = inject(UserStore);

  readonly form = form({
    name: '',
    email: '',
  });

  async save() {
    if (this.form.invalid()) {
      return;
    }

    await this.store.updateUser(this.form.value());
  }
}
</code></pre>
<p>This approach keeps presentation concerns inside the component while allowing business rules to remain centralized in the store.</p>
<h2>Handling Asynchronous Operations</h2>
<p>One challenge when combining Signal Forms with SignalStore is coordinating asynchronous operations.</p>
<p>A form submission typically expects an asynchronous operation to complete before updating its own state. Meanwhile, the store is responsible for managing loading indicators, server errors, and successful updates.</p>
<p>Instead of placing HTTP requests directly inside components, expose descriptive methods such as <code>createUser()</code>, <code>updateProfile()</code>, or <code>changePassword()</code> from the store. Components simply invoke these methods and react to the outcome.</p>
<p>This keeps components lightweight while making business logic reusable across multiple views.</p>
<h2>A Clear Separation of Responsibilities</h2>
<p>A useful guideline is to divide responsibilities as follows:</p>
<p>| Concern             | Recommended Owner                          |
| ------------------- | ------------------------------------------ |
| User input          | Signal Forms                               |
| Validation          | Signal Forms                               |
| Loading remote data | Resource                                   |
| Business rules      | SignalStore                                |
| State mutations     | SignalStore                                |
| HTTP persistence    | Service or repository invoked by the store |</p>
<p>Following these boundaries results in components that focus on presentation, stores that encapsulate business logic, and Resources that handle server synchronization. Each part has a single responsibility, making the application easier to understand, test, and maintain as it grows.</p>
<h2>Migrating from Classic NgRx to SignalStore</h2>
<p>Migrating to SignalStore doesn't require replacing an entire application's state management strategy overnight. In fact, most enterprise applications can adopt SignalStore incrementally while continuing to use the classic NgRx Store where it provides the greatest value.</p>
<p>A practical migration strategy is to start with isolated features rather than the application's global state.</p>
<h3>Keep the Classic Store for Global State</h3>
<p>Global concerns such as authentication, user sessions, application configuration, notifications, and cross-feature communication often continue to benefit from the centralized architecture of the classic NgRx Store.</p>
<p>These areas typically rely on dispatched actions and event-driven workflows that remain well suited to Redux patterns.</p>
<h3>Introduce SignalStore for New Features</h3>
<p>New feature modules are excellent candidates for SignalStore.</p>
<p>Instead of creating actions, reducers, selectors, and effects, developers can define state, computed values, and business methods in a single store. This reduces boilerplate while aligning the feature with Angular's signal-first architecture.</p>
<p>Existing features can also be migrated gradually as they evolve, avoiding large-scale refactoring efforts.</p>
<h3>Move Component State First</h3>
<p>The easiest migration is often replacing component-local Observables and <code>BehaviorSubject</code>s with Signals.</p>
<p>Many components don't require a dedicated store at all. Converting temporary UI state to Signals simplifies the codebase immediately and familiarizes teams with Angular's reactive model before introducing SignalStore.</p>
<p>Incremental adoption minimizes risk while allowing teams to modernize applications at a sustainable pace.</p>
<h2>What This Means for ABP Applications</h2>
<p>Angular 22's signal-first architecture aligns well with ABP's modular application model.</p>
<p>Most ABP applications consist of independent feature modules such as Identity, Tenant Management, SaaS, or CMS. These modules naturally map to feature-scoped SignalStores, allowing state and business logic to remain encapsulated within each module. However, the full support will be introduced in the next version.</p>
<p>As Angular continues investing in Signals, Resources, and Signal Forms, future ABP applications can increasingly rely on the framework's native reactive APIs instead of custom state management patterns.</p>
<p>This doesn't diminish the importance of RxJS or the classic NgRx Store. RxJS remains an essential foundation of Angular's HTTP infrastructure and many third-party libraries, while the traditional Store continues to provide an excellent solution for complex global state management.</p>
<p>Instead, Angular 22 encourages developers to use each reactive tool where it provides the greatest value.</p>
<p>Whether you're upgrading an existing ABP application or starting a new project, adopting SignalStore for feature-level state can simplify development while remaining fully compatible with Angular's evolving ecosystem.</p>
<h2>Conclusion</h2>
<p>Angular's evolution toward a signal-first architecture represents more than a new reactive API—it changes how applications should be designed.</p>
<p>Rather than treating every piece of state as part of a centralized store, Angular now encourages developers to choose the appropriate abstraction for each responsibility. Plain Signals excel at local component state, SignalStore provides a lightweight solution for feature-level business logic, Resources simplify server synchronization, and Signal Forms modernize user input management.</p>
<p>The classic NgRx Store continues to play an important role in large, event-driven applications, but it no longer needs to be the default choice for every state management scenario.</p>
<p>By embracing these complementary tools, developers can build Angular applications that are simpler to maintain, require less boilerplate, and integrate naturally with the framework's latest capabilities.</p>
<p>As Angular continues to evolve around Signals and fine-grained reactivity, adopting these patterns today will help applications remain aligned with the framework's direction while providing a solid foundation for future improvements.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a2229a3-848f-2445-99a0-e7d983f6cc59" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a2229a3-848f-2445-99a0-e7d983f6cc59" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/customizing-the-abp-framework-a-developers-guide-to-leptonx-theme-overrides-in-angular-and-the-transition-to-react-ui-nklweri3</guid>
      <link>https://abp.io/community/posts/customizing-the-abp-framework-a-developers-guide-to-leptonx-theme-overrides-in-angular-and-the-transition-to-react-ui-nklweri3</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>customization</category>
      <category>theme</category>
      <category>abp-framework</category>
      <category>react</category>
      <title>Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI</title>
      <description>Learn how to customize the LeptonX theme in ABP using maintainable, upgrade-safe approaches, and discover how the transition to React gives developers full control over the UI.</description>
      <pubDate>Mon, 29 Jun 2026 05:59:05 Z</pubDate>
      <a10:updated>2026-10-05T09:56:08Z</a10:updated>
      <content:encoded><![CDATA[<h1>Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI</h1>
<p>Enterprise ASP.NET Boilerplate (ABP) projects rarely stay with default theme behavior for long. At some point, teams need stricter brand alignment, user experience (UX) consistency across modules, or product-specific shell behavior that goes beyond palette and typography tweaks.</p>
<p>This article explains a practical way to customize the LeptonX theme in Angular projects through two primary layers :</p>
<ol>
<li><strong>Style Overriding:</strong> Utilizing design tokens, global CSS custom properties (variables), and component-level styling.</li>
<li><strong>Element Overriding:</strong> Replacing or extending UI fragments and layout pieces using ABP's built-in services.</li>
</ol>
<p>Finally, we connect this customization mindset to ABP’s new React direction, where application development teams own more of the user interface (UI) implementation directly from day one.</p>
<h2>Why Overriding Matters in Real ABP Solutions</h2>
<p>In enterprise software engineering, frontend customization is not a cosmetic task. Instead, it directly supports core technical and architectural goals :</p>
<ul>
<li><strong>Brand System Compliance:</strong> Enforcing strict color palettes, layouts, and typography across tenant-facing portals and internal back-office administration pages.</li>
<li><strong>Accessibility (a11y) Improvements:</strong> Optimizing focus states, color contrast ratios, screen reader compatibility, and keyboard navigation to meet WCAG standards.</li>
<li><strong>Product Differentiation:</strong> Structuring distinct top-level layouts, sidebar behavior, and navigation elements to separate multiple products within the same suite.</li>
<li><strong>Operational Usability:</strong> Reorganizing application spaces to match domain-specific workflows and simplify intensive data-entry tasks.</li>
</ul>
<p>To avoid building fragile CSS overrides that break during framework updates, development teams must follow a strict, highly structured hierarchy of customization :</p>
<p>| Level | Customization Type | Technical Mechanism | Strategic Role |
| :---: | :--- | :--- | :--- |
| <strong>1</strong> | <strong>Token-Level Variables</strong> | CSS Custom Properties | 🛡️ <em>First Line of Defense</em> |
| <strong>2</strong> | <strong>Component-Style Patch</strong> | Class-Based Overrides | 🎨 <em>Moderate Visual Tweaks</em> |
| <strong>3</strong> | <strong>Element Replacement</strong> | ReplaceableComponents | 🏗️ <em>Deep Structural Overrides</em> |</p>
<p>Adhering to this hierarchy reduces &quot;style debt&quot; and ensures that theme upgrades remain manageable throughout the application lifecycle.</p>
<h3>Layer 1: Style Overriding in LeptonX (Angular)</h3>
<p>Style overriding is the safest and most maintainable way to alter your application's presentation layer. The LeptonX engine relies heavily on CSS custom properties (variables) defined at the <code>:root</code> level.</p>
<h3>Customizing Brand Colors and Typography Tokens</h3>
<p>To modify the default colors and branding assets, developers can define custom properties within the global <code>src/styles.scss</code> file :</p>
<pre><code class="language-scss">:root {
  /* Set the primary brand color used on active elements, buttons, and focuses */
  --lpx-brand: #1e3a8a;

  /* Set the physical paths for the application logos */
  --lpx-logo: url('/assets/images/logo.png');
  --lpx-logo-icon: url('/assets/images/logo-icon.png'); /* Displayed when sidebar is collapsed */
}
</code></pre>
<p>For applications utilizing multi-theme layouts (such as LeptonX Pro's Light, Dark, or Dim modes), variables can be scoped under individual theme classes to dynamically swap brand colors or assets :</p>
<pre><code class="language-scss">/* Scoping theme-specific logos to prevent visibility issues on dark backgrounds */
:root.lpx-theme-dark, :root.lpx-theme-dim {
  --lpx-logo: url('/assets/images/logo-light.png');
  --lpx-logo-icon: url('/assets/images/logo-icon-light.png');
}
</code></pre>
<h4>Solving the &quot;Visual Branding Blink&quot; on Initial Page Load</h4>
<p>A common issue in production occurs when the default LeptonX logo is briefly displayed on screen before the client browser parses the custom stylesheet. This latency creates a noticeable &quot;blink&quot; or flicker.</p>
<p>To eliminate this rendering gap, bypass the CSS variable load phase by replacing the physical logo assets inside the web host project's public directory. Write your custom branding files directly to <code>/images/logo/leptonx/logo-light.png</code> inside the server's public folder. Because the fallback variable defaults directly to this location, the client browser displays the custom logo asset immediately without waiting to parse the custom CSS rules.</p>
<p>Additionally, note that styles registered solely in the application's global <code>styles.scss</code> may fail to apply to the <strong>Account Layout</strong> (such as the standard login page) because it compiles within an isolated module lifecycle. To ensure your styling overrides apply globally, register the assets and styles in the Virtual File System (VFS) of the.NET backend host, making them universally accessible across all client routing contexts.</p>
<h3>Layer 2: Element Overriding in LeptonX (Angular)</h3>
<p>When CSS modifications cannot support your required user experience (such as adding search interfaces, custom profile controls, or custom action layouts), teams must override the underlying UI elements.</p>
<p>ABP provides the <code>ReplaceableComponentsService</code> to dynamically replace pre-built layout pieces with custom, project-owned Angular components without breaking core module logic.</p>
<h3>Troubleshooting the Mobile User Profile Freeze</h3>
<p>In compiled editions of the LeptonX Lite layout library (specifically versions 3.1.x through 4.3.1), developers have identified a rendering bug affecting mobile layouts. When a user logs in via a mobile device and taps the profile dropdown menu, the page freezes. Instead of displaying the profile options, the sidebar area recursively renders a duplicate copy of the active route page. This layout loop completely breaks navigation until the page is refreshed.</p>
<p>The root cause is a layout bug inside the compiled LeptonX library template (<code>mn-user-profile.component.html</code>), where the template markup is wrapped inside an <code>&lt;ng-component&gt;</code> tag instead of a structurally neutral <code>&lt;ng-container&gt;</code> tag.</p>
<p>To resolve this issue, you can implement a custom component replacement :</p>
<ol>
<li><p>Generate a custom mobile profile component using the Angular CLI</p>
<pre><code class="language-bash">ng g component components/my-mobile-profile
</code></pre>
</li>
<li><p>Implement the component template, ensuring the wrapper elements utilize <code>&lt;ng-container&gt;</code> instead of <code>&lt;ng-component&gt;</code>.</p>
</li>
<li><p>Inject the <code>ReplaceableComponentsService</code> into your root <code>app.component.ts</code> to swap the underlying component keys during application bootstrap :</p>
<pre><code class="language-tsx">import { Component, OnInit } from '@angular/core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { eThemeLeptonXComponents } from '@volosoft/ngx-lepton-x';
import { MyMobileUserProfileComponent } from './components/my-mobile-profile.component';

@Component({
  selector: 'app-root',
  template: '&lt;abp-dynamic-layout /&gt;'
})
export class AppComponent implements OnInit{
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    this.replaceableComponents.add({
      component: MyMobileUserProfileComponent,
      key: eThemeLeptonXComponents.MobileUserProfile
    });
  }
}
</code></pre>
</li>
</ol>
<h3>Template Context: From LeptonX Demo Setup to Real ABP Application Templates</h3>
<p>When transitioning customized designs from local prototypes to production environments, development teams must choose between two operating modes :</p>
<p>| <strong>Operational Mode</strong> | <strong>Core Architecture</strong> | <strong>Rationale &amp; Trade-offs</strong> |
| --- | --- | --- |
| <strong>Standard Template Mode</strong> | Consumes LeptonX packages as standard dependencies (<code>@abp/ng.theme.lepton-x</code>) from npm registries. All overrides are applied at the application layer. | <strong>Highly Recommended.</strong> Keeps local project codebases clean, simplifies dependency updates, and avoids style debt. |
| <strong>Source-Inspection Mode</strong> | Utilizes the ABP CLI <code>get-source</code> command to download the raw theme code and configure temporary local path aliases. | <strong>Diagnostic Only.</strong> Best used for deep debugging, prototyping layout behaviors, or tracing framework-level bugs. |</p>
<h3>Resolving Strict MIME Type CSS Loading Exceptions</h3>
<p>During local development or initial production deployments of LeptonX Lite Angular applications, browsers may refuse to apply the theme's styles. This issue manifests as a console exception:</p>
<p><code>Refused to apply style from 'http://localhost:4200/bootstrap-dim.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.</code></p>
<p>This error occurs when the browser requests static layout stylesheets from paths that do not exist, causing the back-end host to return a default 404 HTML fallback page. To resolve this, run the installation command in your client-side workspace :</p>
<pre><code class="language-bash">abp install-libs
</code></pre>
<p>This command forces the ABP CLI to parse package dependencies, copy the compiled stylesheets directly into the physical output directories, and make them available to the web server.</p>
<h3>Deep Implementation: Integrating Theme Source Code and the Upgrade Trade-Off</h3>
<p>For complex enterprise scenarios requiring structural changes that cannot be achieved via standard token configurations or component replacements, developers have the option to bypass compiled packages entirely and integrate the theme’s raw source code.</p>
<h3>How to Retrieve the Source Code</h3>
<p>ABP Commercial customers have full access to the complete source code of the LeptonX Pro theme. This can be downloaded directly through the ABP Suite user interface or by executing the following command in the ABP CLI within your project directory :</p>
<pre><code>abp get-source Volo.Abp.LeptonXTheme
</code></pre>
<p>This command downloads the raw C# and Angular source files directly into your local solution structure. Once downloaded, you can modify the underlying HTML templates, restructure Angular modules, and alter core layout scripts to meet your product requirements.</p>
<h4>The Upgrade Warning: Maintenance Overhead and Style Debt</h4>
<p>While direct access to the source code provides complete design freedom, it comes with a major warning regarding long-term maintenance :</p>
<ul>
<li><strong>Bypassing the Update Stream:</strong> Once you replace official package references (such as <code>@volosoft/abp.ng.theme.lepton-x</code> or NuGet packages) with local project references, your application is disconnected from the automatic update pipeline.</li>
<li><strong>Manual Merge Burden:</strong> When Volosoft releases framework updates, security patches, or compatibility fixes (such as aligning with newer Angular or.NET compiler baselines), these updates will not automatically apply to your customized code. Your team must manually compare, diff, and merge upstream changes, which can introduce regressions and increase technical debt.</li>
<li><strong>VFS and APIs as the First Line of Defense:</strong> Before choosing a full source code integration, try using the Virtual File System (VFS) on the backend or standard component replacement APIs in the frontend to override only the specific elements you need to change. This allows you to customize the UI while keeping the rest of your theme packages fully upgradeable.</li>
</ul>
<h3>Connecting the Mindset to ABP’s New React Era</h3>
<p>The introduction of the React UI option in ABP 10.4 represents a major architectural shift. While the Angular implementation relies on structured layout packages and runtime component overrides, the React architecture prioritizes <strong>direct developer ownership</strong> of the presentation layer.</p>
<pre><code class="language-mermaid">graph TD
    %% Styling
    classDef react fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#0d47a1;
    classDef dotnet fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#4a148c;
    classDef proxy fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#e65100;
    classDef tool fill:#f5f5f5,stroke:#757575,color:#333;

    %% React App Box
    subgraph ReactApp [&quot;React App Repository&quot;]
        C1[&quot;Custom Business Components&lt;br&gt;&lt;small&gt;(Local Source Code)&lt;/small&gt;&quot;]:::react
        C2[&quot;TanStack Router &amp; Query&lt;br&gt;&lt;small&gt;(Type-Safe Client Routes)&lt;/small&gt;&quot;]:::react
        
        T1[&quot;Vite Dev Server &amp; Bundling&lt;br&gt;&lt;small&gt;(Fast HMR, Vitest)&lt;/small&gt;&quot;]:::tool
        T2[&quot;Tailwind CSS / shadcn/ui&lt;br&gt;&lt;small&gt;(Accessible UI Components)&lt;/small&gt;&quot;]:::tool

        C1 --&gt; C2
        T1 --&gt; T2
    end

    %% Backend Box
    subgraph NetCore [&quot;ASP.NET Core Web API Host&quot;]
        P1[&quot;Dynamic API Client Proxies&lt;br&gt;&lt;small&gt;(Auto-Generated Endpoints)&lt;/small&gt;&quot;]:::proxy
        A1[&quot;ABP Admin Console&lt;br&gt;&lt;small&gt;(Delivered via NuGet)&lt;/small&gt;&quot;]:::dotnet

        P1 &lt;==&gt; A1
    end

    %% Inter-Repository Flow
    ReactApp -- &quot;Generates Dynamic Proxies&quot; --&gt; P1

    %% Layout Tweaks
    style ReactApp fill:#fafafa,stroke:#1e88e5,stroke-width:1px,stroke-dasharray: 5 5;
    style NetCore fill:#fafafa,stroke:#8e24aa,stroke-width:1px,stroke-dasharray: 5 5;
</code></pre>
<h3>What Stays Consistent vs. What Changes</h3>
<p>Understanding how patterns transfer between frameworks is key for teams migrating to the React UI:</p>
<ul>
<li><strong>What Stays Consistent:</strong> Core DDD infrastructure, backend integration, dynamic API proxy generation, multi-tenancy models, and permission-aware routing configurations.</li>
<li><strong>What Changes:</strong> Direct ownership of page layouts, faster iteration of UI composition, and modern utility-first styling tools.</li>
</ul>
<h3>A New Frontend Philosophy</h3>
<p>In the Angular model, developers import pre-built layouts from compiled packages and selectively override elements using classes or replacing components. While structured, this approach can sometimes feel like &quot;fighting&quot; the framework.</p>
<p>The React UI model, by contrast, gives developers direct control over the UI components from day one. Standard administrative pages (such as Identity, Tenants, and Settings) are managed separately by the <strong>ABP Admin Console</strong> on the back-end host, while all application layouts and views remain locally in your React project.</p>
<p>Built with modern tools like <strong>Vite</strong>, <strong>Tailwind CSS</strong>, and <strong>shadcn/ui</strong>, developers can customize and extend components directly in their local source files without needing complex overriding wrappers.</p>
<p>Additionally, because the layout and page templates reside in local source directories rather than compiled packages, this architecture is highly optimized for AI-driven development. Automated coding agents (such as the ABP Studio AI Agent) can easily inspect and modify local layouts, run API proxy generation, and deploy updates quickly.</p>
<p>Whether your enterprise solution leverages the structured, component-driven architecture of ABP's Angular UI or is stepping into the modern, developer-owned era of the Vite-powered React UI , establishing an intentional, upgrade-safe customization strategy is crucial. By resolving design changes through token-level custom properties first, documenting structural element overrides, and preparing public-facing technical resources to be highly citable by conversational search agents , development teams can insulate their codebases from technical debt. Ultimately, the transition from rigid theme packages to direct frontend ownership not only streamlines day-to-day software delivery but also ensures that your application framework remains flexible, performant, and visible in an AI-driven ecosystem.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a222422-9556-2b58-caf6-86f4deb82a46" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a222422-9556-2b58-caf6-86f4deb82a46" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/template-in-product-out-building-hanova-with-the-abp-ai-agent-hcntpk3j</guid>
      <link>https://abp.io/community/posts/template-in-product-out-building-hanova-with-the-abp-ai-agent-hcntpk3j</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp-studio</category>
      <category>template</category>
      <category>ai</category>
      <category>sample</category>
      <title>Template In, Product Out: Building Hanova with the ABP AI Agent</title>
      <description>Hanova is a home-services booking sample application built with the ABP AI Coding Agent in Studio. This article explores why “fast” alone wasn’t enough, how separating template and product concerns streamlined the work, and the workflow that proved most effective: plan → verify → agent → verify with demo seed.</description>
      <pubDate>Mon, 25 May 2026 13:45:36 Z</pubDate>
      <a10:updated>2026-10-05T09:30:05Z</a10:updated>
      <content:encoded><![CDATA[<h1>Template In, Product Out: Building Hanova with the ABP AI Agent</h1>
<p>Generic AI coding tools can write code really fast. They often leave chunks that do not fit your framework and become expensive to maintain later. The <a href="https://abp.io/studio/ai-agent">ABP AI Coding Agent</a> in ABP Studio aims at a different outcome. Hence, it understands ABP solution structure, follows project rules, plans before large changes, and leaves a codebase you can always extend.</p>
<h2>This article is a real build story. <strong>Hanova</strong> is a home-services booking sample serving customer and provider roles, using MongoDB, Redis, SignalR, React Native mobile UI, and demo seed data for both personas on first migrate.</h2>
<h2>1. Why “fast” is not enough</h2>
<p>Hanova is built on the ABP Framework: pick a role, browse open jobs or specialists, send a request, negotiate the price, and message through to confirmation. Log in as <code>ayse.kaya</code> or <code>mehmet.yilmaz</code> (password <code>Demo@1234</code>) after a single database migrate, and every tab already has something on it.</p>
<table>
  <tr>
    <td align="center" width="33%"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/hanova-hook-1.png" alt="Hanova — role selection" /></td>
    <td align="center" width="33%"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/hanova-hook-2.png" alt="Hanova — bookings" /></td>
    <td align="center" width="33%"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/hanova-hook-3.png" alt="Hanova — messaging" /></td>
  </tr>
</table>
<p>That end-to-end loop is what I wanted to ship. What I did <em>not</em> want was a repository that only looked finished on day one.</p>
<h3>The speed trap</h3>
<p>AI-assisted development is good at the first sunny-day build. Ask for a booking screen, a REST endpoint, a chat list,and you get code quickly. The problem shows up on the <em>second</em> request: “Add negotiation,” “Wire SignalR,” “Enforce permissions on confirm,” “Seed demo users so QA can log in.”</p>
<p>Without framework context, each prompt tends to invent its own pattern:</p>
<ul>
<li>A new API style instead of an application service + permission</li>
<li>Direct database access instead of repositories</li>
<li>A one-off WebSocket layer instead of extending the hub already in the module</li>
</ul>
<p>The app may still run. However, every new feature fights the last one. Review time goes up. The next developer, or the next agent session spend half the effort re-learning what the previous session improvised. That is <strong>fast but fragile</strong>. In other words you sustain the velocity today, but you will have to do the rework tomorrow.</p>
<h3>What “efficient” and “sustainable” meant here</h3>
<p>I used the ABP AI Agent inside Studio, not as generic autocomplete, but as a teammate that already knows where entities, app services, permissions, and Mongo collections live in a single-layer solution.</p>
<p>The goal was <strong>fast and sustainable</strong>:</p>
<ul>
<li>New work lands in the same folders and conventions as the template</li>
<li>Bookings, messaging, and negotiation share one lifecycle and one real-time hub</li>
<li>Demo data stays idempotent so migrate-and-run stays trustworthy</li>
<li>The next feature extends the same graph instead of patching around it</li>
</ul>
<p>What made agent-assisted development stick was not raw generation speed. It was working inside ABP’s structure with plans, project rules, skills, and safety rails. So, the codebase still reads like an ABP application even months later.</p>
<p><strong>Takeaway:</strong> Treat AI as a delivery accelerator only when it preserves your framework conventions. Otherwise you trade tomorrow’s velocity for today’s demo.</p>
<hr />
<h2>2. Template vs. product</h2>
<p>Hanova was scaffolded from the <strong>ABP single-layer application template</strong>: one .NET project, MongoDB, OpenIddict auth, Admin Console, React SPA scaffold, React Native shell, and English + Turkish localization. That is a lot of plumbing. It is also not the product.</p>
<h3>What the template already carried</h3>
<p>| Area | Already in the box |
|------|-------------------|
| Identity &amp; auth | Users, roles, OpenIddict clients, token flow |
| Authorization | Permission groups, role seeding (<code>Customer</code>, <code>Provider</code>) |
| Host &amp; ops | Run profiles, <code>--migrate-database</code>, Docker files |
| Mobile &amp; web shell | Expo auth/tabs/settings; Vite React login and identity |
| Sample CRUD | <strong>Books</strong> — proof that entity → app service → UI works |</p>
<p>The Books sample is just a <strong>reference slice</strong>, not product scope. Hanova’s booking flow follows the same shape. The domain changed, but the skeleton did not. Login, OAuth, theming, and navigation did not need to be re-specified in every prompt.</p>
<h3>What the agent had to grow</h3>
<p><strong>Backend:</strong> service categories, customer and provider profiles, service areas, bookings, negotiation, messaging hub, and supporting domains (payments, settlements, verification) toward full workflows.</p>
<p><strong>Mobile (primary UI):</strong> role entry, customer tabs (Discovery, Bookings, Messages, Account), provider tabs (Job feed, Bookings, Messages, Earnings, Account), plus booking, negotiation, chat, and profile screens.</p>
<p><strong>Demo glue:</strong> two personas, pending and confirmed bookings, and a message thread so migrate-and-run populates every tab.</p>
<blockquote>
<p><strong>Template:</strong> auth, permissions, navigation, theming, sample CRUD pattern.<br />
<strong>Product:</strong> who books whom, for what service, at what price, with what conversation attached.</p>
</blockquote>
<p>When a prompt said “add provider job feed,” the answer was not a new auth stack. It was a new app service, permissions, and screens <strong>inside</strong> existing patterns.</p>
<hr />
<h2>3. Why a framework-native agent matters</h2>
<p>Once the assistant was ABP-native inside Studio, day-to-day work changed. The agent sees module layout, run profiles, permissions, and Mongo registration <strong>before</strong> it edits. For Hanova, that meant fewer wrong first drafts and fewer “throw this away and wire it properly” passes.</p>
<h3>One workspace instead of five tabs</h3>
<p>A typical feature would have to cross backend, mobile, and ops. Simply; add a permission, run migrate after seed changes, reload Expo, read the runtime monitor when SignalR did not connect. In Studio, the same session moves from “implement confirm rules” to “run migrator” to “why did this 403?” without re-explaining the whole stack each time.</p>
<h3>Semantic search over a growing graph</h3>
<p>A booking links to a provider profile, a conversation, hub groups, and mobile state. Prompts rarely name every path. Indexed search tended to land on existing job feed, booking, and hub code instead of inventing parallel endpoints. Generic tools often solve the literal sentence, not the graph it sits in.</p>
<h3>What the agent could lean on</h3>
<p>| Hanova need | Agent advantage |
|-------------|-----------------|
| Booking + confirm rules | Same vertical pattern as Books; permissions on mutating operations |
| Provider job feed | Query existing bookings by provider specializations—not a second “job” store |
| Messaging &amp; negotiation | Extend the existing messaging hub, not a new socket stack |
| Runnable demo | <code>--migrate-database</code> and idempotent seed personas |
| Auth or SignalR failures | Runtime monitor output fed back into the same chat |</p>
<p>Efficiency came from <strong>correct first guesses</strong> in ABP-shaped folders. So, this is beyond typing speed alone.</p>
<hr />
<h2>4. Keeping the codebase maintainable</h2>
<p>Speed only pays off if the repo is still understandable after the tenth session. Sustainability meant every agent turn <strong>adds to the same architecture</strong>, not forks a new one.</p>
<h3>Plan before Agent mode</h3>
<p>Multi-surface work needs a shared map first. <strong>Plan mode</strong> produces affected files, steps, and test notes before edits.</p>
<p>| Without a plan | With an approved plan |
|----------------|----------------------|
| Orphan DTOs with no app service | Full vertical slice through API and permissions |
| A second hub for “quick” push | Extend the existing messaging hub |
| Mobile calling an unauthorized endpoint | Permission grants listed as plan steps |</p>
<p><strong>There is no multi-entity Agent running without a checked plan.</strong></p>
<h3>Rules, guardrails, and vertical slices</h3>
<p>Every new chat starts with zero memory. <strong>Project rules</strong> (ABP conventions + Hanova-specific orientation) encode how we build: repositories in app services, localized business exceptions, Mapperly mappings are not renegotiated each session.</p>
<p>| Control | Role |
|---------|------|
| <code>.abpignore</code> | Keeps secrets and certs out of agent context |
| AI Scopes | Backend vs mobile folders when refactoring |
| Permission prompts | Shell and fetch require approval with a reason |
| Git snapshot revert | Roll back a bad turn without diff archaeology |</p>
<hr />
<h2>5. Lessons learnt — one example (provider job feed)</h2>
<p>The job feed is where a provider sees customers’ open booking requests where the clearest place to see the full loop in practice.</p>
<h3>What we did</h3>
<p>| Step | What happened |
|------|----------------|
| 1. <strong>Plan</strong> | Reuse existing bookings (no duplicate “job” table), filters, API + mobile screen, permissions, expected demo outcome |
| 2. <strong>Verify the plan</strong> | Read, adjust, <strong>approve</strong>, no code until this passes |
| 3. <strong>Agent</strong> | Implement, migrate, start API; fix permission error using runtime monitor in the same chat |
| 4. <strong>Verify the implementation</strong> | Seeded provider → Jobs tab shows matching open requests—not all, not none |
| 5. <strong>Recover</strong> <em>(if needed)</em> | Snapshot revert + narrower <strong>AI Scope</strong>, same plan |</p>
<h3>Studio setup around the slice</h3>
<p>These controls mattered as much as the prompt:</p>
<ul>
<li><strong>Rules &amp; workflows</strong> — ABP single-layer conventions and a repeatable slice checklist</li>
<li><strong>Skills</strong> — inject the checklist so each session does not start from zero</li>
</ul>
<table>
  <tr>
    <td align="center" width="50%"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/studio-import-skills.png" alt="ABP Studio — Import Skills dialog for Hanova conventions" /></td>
    <td align="center" width="50%"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/studio-rules-skills.png" alt="ABP Studio — Rules &amp; Skills configured for Hanova" /></td>
  </tr>
</table>
<ul>
<li><strong>AI Scope</strong> — jobs API + provider screens only; smaller scope on recover</li>
</ul>
<table>
  <tr>
    <td align="center"><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-05-25-Building-Hanova-with-the-ABP-AI-Agent/images/studio-ai-scope.png" alt="ABP AI Agent — Scope Settings with Screens scope selected for Hanova" /></td>
  </tr>
</table>
<ul>
<li><strong>Models &amp; thinking</strong> — lighter for Plan/review, deeper for cross-layer Agent work</li>
<li><strong>MCP</strong> (optional) — extra context when the answer lives outside the repo</li>
<li><strong><code>.abpignore</code></strong> — secrets stay out of context</li>
</ul>
<h3>What we learnt from this slice</h3>
<p><strong>The plan had to cover the whole slice, not just the API.</strong> Permissions, role grants in seed data, and the mobile list were all part of job feed. Reviewing the plan caught the grant step before Agent mode. Otherwise, the provider hits “access denied” even when the API looks finished.</p>
<p><strong>Plan the API and the screen together.</strong> Filters exist on the phone and on the server. Backend-only plans often yield a working API and a list that shows nothing, or everything.</p>
<p><strong>Know what “working” looks like before you test.</strong> Demo data defines success: several open customer requests; provider set up for plumbing and electrical work. Write that into the plan so verification is pass/fail.</p>
<p><strong>Recover execution, keep the plan.</strong> When a session edits unrelated auth settings, revert and retry with a tighter scope rather than  throwing away the approved plan.</p>
<p>Negotiation, messaging, and other features followed the same loop.</p>
<hr />
<h2>6. ABP AI Agent vs generic coding assistants</h2>
<p>Generic tools (Cursor, Claude Code, Windsurf) are strong for editing code. The ABP agent is built for <strong>ABP delivery inside Studio</strong>. The goal is similar, but the default context is quite different. Hanova is one of the proof case. It is not about “who writes faster,” but <strong>what you re-do less</strong>.</p>
<p>| Dimension | Generic assistant | ABP AI Agent (Hanova) |
|-----------|-------------------|------------------------|
| Solution shape | Inferred from open files | Single-layer layout, modules, run profiles in context |
| Permissions | Often missing or hardcoded | Defined, authorized, and seeded as part of the slice |
| Database &amp; demo data | Easy to pick the wrong approach | <code>--migrate-database</code>, seed contributors, idempotent demo users |
| Real-time features | Temptation to add a parallel socket stack | Extend existing hub and module wiring |
| Docs &amp; conventions | Web search or pasted snippets | ABP docs subagent + project rules and workflows |
| Session control | Usually whole repo | AI Scopes, <code>.abpignore</code>, approval prompts |
| When a turn goes wrong | Git history | Git + per-turn snapshot revert |
| Run &amp; debug | Separate terminal / browser | Start app, migrate, runtime monitor in the same chat |</p>
<p>Generic tools still excel at quick edits and experiments in any stack. We are not claiming Studio replaces them. Use a generic assistant when the problem is “code.” Use the ABP agent when the problem is <strong>shipping an ABP feature</strong> end to end.</p>
<hr />
<h2>7. Template in, product out</h2>
<p>Hanova started as an ABP template and became a working app: two roles, bookings, messaging, demo data on first migrate. The agent did not replace thinking, but it significantly <strong>shortened the gap</strong> between a feature idea and something you can run, review, and extend without breaking coding conventions.</p>
<p><strong>Who this workflow fits</strong></p>
<ul>
<li><strong>Developers</strong> — Plan → verify plan → Agent → verify with demo data; rules early, scopes when a turn goes wide</li>
<li><strong>Team leads</strong> — Shared workflows, <code>.abpignore</code>, snapshot policy, scopes</li>
<li><strong>Product owners</strong> — Plans as reviewable artifacts; demo seed as a visible acceptance check</li>
</ul>
<p>Hanova still has room to grow (payments, settlements, verification). The agent accelerates <strong>slices you prioritize</strong>, not the whole backlog at once.</p>
<p><strong>You can try it yourself:</strong> <a href="https://abp.io/studio">Download ABP Studio</a> · <a href="https://abp.io/studio/ai-agent">ABP AI Coding Agent</a></p>
<p>The template carried authentication and navigation. The agent carried what turned Hanova into a product inside ABP, not beside it.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21718f-1e30-b59b-5024-af7f62ca7d0a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21718f-1e30-b59b-5024-af7f62ca7d0a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/automate-localhost-access-for-expo-a-guide-to-dynamic-cloudflare-tunnels-dev-builds-7cblqtj3</guid>
      <link>https://abp.io/community/posts/automate-localhost-access-for-expo-a-guide-to-dynamic-cloudflare-tunnels-dev-builds-7cblqtj3</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>mobil-application</category>
      <category>tutorial</category>
      <category>proxy-system</category>
      <category>react</category>
      <category>api</category>
      <title>Automate Localhost Access for Expo: A Guide to Dynamic Cloudflare Tunnels &amp; Dev Builds</title>
      <description>Stop hardcoding local IPs and wrestling with SSL errors. This guide shows you how to bridge your local backend to physical mobile devices using Cloudflare Tunnels and Expo. Learn to use a Node.js script that dynamically captures secure HTTPS URLs and syncs them directly to your React Native environment. Perfect for testing OAuth flows and debugging on real hardware, this automated workflow ensures your local API is always reachable and secure without manual configuration.</description>
      <pubDate>Mon, 09 Mar 2026 10:55:04 Z</pubDate>
      <a10:updated>2026-10-05T06:42:32Z</a10:updated>
      <content:encoded><![CDATA[<h1>Automate Localhost Access for Expo: A Guide to Dynamic Cloudflare Tunnels &amp; Dev Builds</h1>
<p>Every mobile developer eventually hits the &quot;Localhost Wall.&quot; You have built a brilliant API on your machine, and your React Native app works perfectly in the iOS Simulator or Android Emulator. But the moment you pick up a physical device to test real-world performance or camera features, everything breaks.</p>
<h3>The Problem: Why Your Phone Can’t See localhost</h3>
<p>When you run a backend server on your computer, localhost refers to the &quot;loopback&quot; address and essentially, the computer talking to itself. Your physical iPhone or Android device is a separate node on the network. From its perspective, localhost is itself, not your development machine. Without a direct bridge, your mobile app is shouting into a void, unable to reach the API sitting just inches away on your desk.</p>
<h3>The Conflict: The Fragility of Local IP Addresses</h3>
<p>The traditional workaround is to find the local IP address of your device and hardcode it into your app. However, this approach has many obstacles that make it difficult to use:</p>
<ul>
<li><strong>Network Volatility:</strong> Your router might assign you a new IP address tomorrow, forcing you to update your code constantly.</li>
<li><strong>The SSL Headache:</strong> Modern mobile operating systems and many OAuth providers (like Google or Auth0) strictly require <strong>HTTPS</strong>. Running a local development server with valid SSL certificates is a notorious configuration nightmare.</li>
<li><strong>Broken OAuth flows:</strong> Most authentication providers refuse to redirect to a non-secure <code>http</code> address or a random local IP, effectively locking you out of testing login features on a real device.</li>
</ul>
<h3>The Solution: Cloudflare Tunnel as a Secure Bridge</h3>
<p>This is where <strong>Cloudflare Tunnel</strong> changes the game. Instead of poking holes in your firewall or wrestling with self-signed certificates, Cloudflare Tunnel creates a secure, outbound-only connection between your local machine and the Cloudflare edge.</p>
<p>It provides you with a <strong>public, HTTPS-enabled URL</strong> (e.g., <code>https://random-word.trycloudflare.com</code>) that automatically points to your local port. To your mobile device, your local backend looks like a standard, secure production API. It bypasses network restrictions, satisfies SSL requirements, and—when paired with a simple automation script—makes &quot;localhost&quot; development on physical devices completely seamless.</p>
<h3>1. Architecture Overview</h3>
<p>In order to understand why this setup is so effective, it is better to visualize the data flow. Traditionally, your mobile device would try to ping your laptop directly over Wi-Fi that is often blocked by firewalls or complicated by internal IP routing.</p>
<h4>Workflow Summary: The Secure &quot;Middleman&quot;</h4>
<p>The Cloudflare Tunnel acts as a persistent, encrypted bridge between your local environment and the public internet. Here is how the traffic flows in a standard development session:</p>
<ol>
<li><strong>The Connector:</strong> You run a small  <code>cloudflared</code> daemon on your development machine. It establishes an <strong>outbound</strong> connection to Cloudflare’s nearest edge server. Because it is outbound, you don't need to open any ports on your home or office router.</li>
<li><strong>The Public Endpoint:</strong> Cloudflare provides a temporary, unique HTTPS URL (e.g., <code>https://example-tunnel.trycloudflare.com</code>). This URL is globally accessible.</li>
<li><strong>The Mobile Request:</strong> Your React Native app that is running on a physical iPhone or Android sends an API request to that HTTPS URL. To the phone, this looks like any other secure production website.</li>
<li><strong>The Local Handoff:</strong> Cloudflare receives the request and &quot;tunnels&quot; it down the active connection to your machine. The <code>cloudflared</code> tool then forwards that request to your local backend whether it's running on <code>.NET</code> at port <code>44358</code>, <code>Node.js</code> at <code>3000</code>, or <code>Rails</code> at <code>3000</code>.</li>
<li><strong>The Response:</strong> Your backend processes the request and sends the data back through the same tunnel to the phone.</li>
</ol>
<p>By sitting in the middle, Cloudflare handles the <strong>SSL termination</strong> and the <strong>Global Routing</strong>, ensuring your backend is reachable regardless of whether your phone is on the same Wi-Fi as your laptop.</p>
<h3>2. Prerequisites</h3>
<p>Before we bridge the gap between your mobile device and your local machine, ensure your development environment is equipped with the following core components.</p>
<p>To follow this guide, you will need:</p>
<ul>
<li><strong>Node.js &amp; Package Manager:</strong> A stable version of Node.js (LTS recommended) and either <strong>npm</strong> or <strong>yarn</strong> to manage dependencies and run the automation scripts.</li>
<li><strong>Expo CLI:</strong> Ensure you have the latest version of <code>expo</code> installed globally or within your project. We will be using this to manage the development server and build the application.</li>
<li><strong>Cloudflared CLI:</strong> This is the critical &quot;connector&quot; tool from Cloudflare. You’ll need it installed on your local machine to establish the tunnel.
<ul>
<li><em>Quick Tip:</em> You don't need a paid Cloudflare account; the <strong>Quick Tunnels</strong> used in this guide are free and require no login.</li>
</ul>
</li>
<li><strong>A Running Backend API:</strong> Your local server (e.g., .NET, Node.js, Django, or Rails) should be active and listening on a specific port (like <code>44358</code> or <code>3000</code>).</li>
</ul>
<h3>3. Step-by-Step Implementation</h3>
<p>Now, let’s configure the automation that makes this workflow &quot;set it and forget it.&quot;</p>
<h4>Phase A: Backend Configuration (The OAuth Handshake)</h4>
<p>Modern mobile authentication often relies on <strong>OAuth 2.0</strong> or <strong>OpenID Connect</strong>. For the login flow to succeed, your backend must &quot;trust&quot; the redirect URI sent by the mobile app. ABP applications are an example for such handshake.</p>
<p>Even though we are using a Cloudflare URL for the API calls, the <code>auth-session</code> of Expo typically generates a <code>localhost</code> redirect for development. You must update your backend configuration (e.g., <code>appsettings.json</code> in a .NET TemplateTwo setup) to allow this:</p>
<p><strong>File:</strong> <code>src/YourProject.DbMigrator/appsettings.json</code></p>
<pre><code class="language-json">{
  &quot;OpenIddict&quot;: {
    &quot;Applications&quot;: {
      &quot;Mobile_App&quot;: {
        &quot;ClientId&quot;: &quot;Mobile_App&quot;,
        &quot;RootUrl&quot;: &quot;exp://localhost:19000&quot;
      }
    }
  }
}
</code></pre>
<p><strong>Note:</strong> By setting the <code>RootUrl</code> to <code>exp://localhost:19000</code>, you ensure that once the user authenticates via the tunnel's secure page, the mobile OS knows exactly how to hand the token back to your running Expo instance.</p>
<h4>Phase B: The &quot;Magic&quot; Script (Automating the Tunnel)</h4>
<p>The primary headache with free Cloudflare Tunnels is that they generate a <strong>random URL</strong> every time you restart the service. Manually copying <code>https://shiny-new-url.trycloudflare.com</code> into your frontend code every morning is a productivity killer.</p>
<p>We solve this with a <strong>Node.js automation script</strong> that launches the tunnel, &quot;listens&quot; to the terminal output to find the new URL, and automatically injects it into your project's configuration.</p>
<p><strong>File:</strong> <code>react-native/scripts/tunnel.js</code></p>
<pre><code class="language-js">const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

// Target files for automation
const tunnelConfigFile = path.join(__dirname, '..', 'tunnel-config.json');
const environmentFile = path.join(__dirname, '..', 'Environment.ts');

// 1. Launch the Cloudflare Tunnel pointing to your local API port
const cloudflared = spawn('cloudflared', ['tunnel', '--url', 'http://localhost:44358']);

let domainCaptured = false;

cloudflared.stdout.on('data', data =&gt; {
  const output = data.toString();
  console.log(output); // Keep logs visible for debugging

  if (!domainCaptured) {
    // 2. Regex to catch the dynamic &quot;trycloudflare&quot; URL
    const urlMatch = output.match(/https:\/\/([a-z0-9-]+\.trycloudflare\.com)/);
    if (urlMatch) {
      const domain = urlMatch[1];
      
      // 3. Save to a JSON file for the app to read
      fs.writeFileSync(tunnelConfigFile, JSON.stringify({ domain }, null, 2));
      
      // 4. Update the fallback value in Environment.ts directly
      let envContent = fs.readFileSync(environmentFile, 'utf8');
      envContent = envContent.replace(
        /let tunnelDomain = '[^']*'; \/\/ fallback/,
        `let tunnelDomain = '${domain}'; // fallback`,
      );
      fs.writeFileSync(environmentFile, envContent, 'utf8');
      
      console.log(`\n✅ Tunnel Synchronized: ${domain}`);
      domainCaptured = true;
    }
  }
});
</code></pre>
<p>By capturing the trycloudflare.com domain programmatically, we treat the tunnel like a dynamic environment variable. This ensures that your mobile app, your backend OAuth settings, and your API client stay in perfect sync without a single keystroke from you.</p>
<h4>Phase C: Environment Integration</h4>
<p>To make this work within your React Native code, your <code>Environment.ts</code> file needs to be &quot;smart&quot; enough to look for the generated config file. We use a <code>try/catch</code> block so the app doesn't crash if the tunnel isn't running.</p>
<p><strong>File:</strong> <code>react-native/Environment.ts</code></p>
<pre><code class="language-tsx">let tunnelDomain = 'your-default-fallback.com'; // fallback

try {
  // Pull the latest domain from the script's output
  const tunnelConfig = require('./tunnel-config.json');
  if (tunnelConfig?.domain) {
    tunnelDomain = tunnelConfig.domain;
  }
} catch (e) {
  console.warn('⚠️ No active tunnel config found. Using fallback.');
}

const apiUrl = `https://${tunnelDomain}`;

export const getEnvVars = () =&gt; {
  return {
    apiUrl,
    // Other environment variables...
  };
};
</code></pre>
<p>This setup creates a <strong>&quot;Single Source of Truth.&quot;</strong> When you run the script, it updates <code>tunnel-config.json</code>, and your app instantly points to the correct secure endpoint.</p>
<h3>4. Integration with Expo Development Builds</h3>
<p>While you can technically use the standard <strong>Expo Go</strong> app for basic API testing, professional React Native workflows, especially those involving secure authentication and custom networking, rely on <strong>Expo Development Builds</strong>.</p>
<h4>Why Development Builds are Essential for This Workflow</h4>
<p>Standard Expo Go is a &quot;one-size-fits-all&quot; sandbox. However, as your app grows, it needs to behave more like a real, standalone binary. Development Builds are preferred for two main reasons:</p>
<ul>
<li><strong>Custom URL Schemes:</strong> For OAuth flows (like the one configured in Phase A), your app needs to handle specific deep links (e.g., <code>myapp://</code>). Expo Go has its own internal URL handling that can sometimes conflict with complex redirect logic. A Development Build allows you to define your own scheme, ensuring the Cloudflare-tunneled backend knows exactly where to send the user back after login.</li>
<li><strong>Native Dependency Control:</strong> If your app uses native modules for secure storage, biometrics, or advanced networking, Expo Go won't support them. A Development Build includes your project's specific native code while still giving you the &quot;hot reloading&quot; developer experience of Expo.</li>
</ul>
<h4>Configuring the Build for Tunnelling</h4>
<p>To ensure your development build is ready for the Cloudflare tunnel, you'll typically use the <code>expo-dev-client</code> package. This transforms your app into a powerful developer tool that can switch between different local or tunneled environments on the fly.</p>
<blockquote>
<p><strong>Pro Tip:</strong> When you run <code>npx expo start</code>, your Development Build will look for the <code>apiUrl</code> we configured in <code>Environment.ts</code>. Since our script has already injected the Cloudflare URL, the physical device will connect to your local backend through the tunnel the moment the app loads.</p>
</blockquote>
<h3>5. Execution Workflow</h3>
<p>To get your entire stack synchronized, follow this specific launch order. This ensures the tunnel is active and the configuration files are updated before the React Native app attempts to read them.</p>
<h4>Step 1: Start the Backend</h4>
<p>Fire up your API (e.g., <code>.NET</code>, <code>Node</code>, <code>Go</code>). Ensure it is listening on the port defined in your <code>tunnel.js</code> (e.g., <code>44358</code>).</p>
<h4>Step 2: Launch the Tunnel</h4>
<p>In a new terminal, run your automation script.</p>
<p>Wait for the message: <code>✅ Tunnel Synchronized</code>. This confirms <code>tunnel-config.json</code> has been updated with the new <code>trycloudflare.com</code> domain.</p>
<h4>Step 3: Start Expo</h4>
<p>Finally, start your Expo development server:</p>
<pre><code class="language-bash">npx expo start
</code></pre>
<p>Open the app on your physical device by scanning the QR code. Your app is now communicating with your local machine over a secure, global HTTPS bridge.</p>
<h3>6. Troubleshooting &amp; Best Practices</h3>
<p>Even with automation, networking can be finicky. If your app isn't reaching the API, check these common roadblocks:</p>
<h4>Common Pitfalls</h4>
<ul>
<li><strong>Port Mismatches:</strong> Ensure the port in your <code>tunnel.js</code> script (e.g., <code>44358</code>) exactly matches the port your backend is listening on. If your backend uses HTTPS locally, ensure the tunnel command reflects that (e.g., <code>https://localhost:port</code>).</li>
<li><strong>Firewall &amp; Ghost Processes:</strong> Sometimes a previous <code>cloudflared</code> process hangs in the background. If you can't start a new tunnel, kill existing processes or check if your local firewall is blocking <code>cloudflared</code> from making outbound connections.</li>
<li><strong>Expired Sessions:</strong> Free &quot;Quick Tunnels&quot; are temporary. If you leave your computer on overnight, the tunnel might disconnect. Simply restart the script to generate a fresh, synced URL.</li>
</ul>
<h4>Security Note</h4>
<p>Cloudflare Tunnels create a <strong>publicly accessible URL</strong>. While the random strings in <code>trycloudflare.com</code> provide &quot;security through obscurity,&quot; anyone with that link can hit your local API.</p>
<ul>
<li><strong>Development Data Only:</strong> Never use this setup with production databases or sensitive PII (Personally Identifiable Information).</li>
<li><strong>Disable When Idle:</strong> Close the tunnel terminal when you aren't actively developing to shut the &quot;bridge&quot; to your machine.</li>
</ul>
<h3>7. Conclusion &amp; Future-Proofing</h3>
<p>By replacing hardcoded local IPs with a dynamic Cloudflare Tunnel, you’ve transformed a clunky, manual process into a <strong>&quot;Set it and forget it&quot;</strong> workflow. You no longer have to worry about shifting Wi-Fi addresses or SSL certificate errors on physical devices. Your development environment now mirrors the behavior of a production app, providing more accurate testing and faster debugging.</p>
<h4>The Road to Production: EAS</h4>
<p>This tunneling strategy is the perfect companion for <strong>EAS (Expo Application Services)</strong>. As you move toward testing internal distributions, you can use these same environment patterns to point your EAS-built binaries to various staging or development endpoints.</p>
<p>With a secure bridge and an automated config, you are no longer tethered to a simulator. Grab your phone, head to a coffee shop, and keep building—your backend is now globally (and securely) following you.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1fe469-52c2-9c24-575a-448551e9c4fa" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1fe469-52c2-9c24-575a-448551e9c4fa" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/angular-library-linking-made-easy-paths-workspaces-and-symlinks-5z2ate6e</guid>
      <link>https://abp.io/community/posts/angular-library-linking-made-easy-paths-workspaces-and-symlinks-5z2ate6e</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>cli</category>
      <category>application-development</category>
      <category>best-practices</category>
      <title>Angular Library Linking Made Easy: Paths, Workspaces, and Symlinks</title>
      <description>A complete guide to managing local path references and libraries in Angular using the new Application Builder — covering path mapping, linking strategies, and best practices for modern Angular workspaces.</description>
      <pubDate>Fri, 17 Oct 2025 06:59:42 Z</pubDate>
      <a10:updated>2026-10-05T11:03:56Z</a10:updated>
      <content:encoded><![CDATA[<h1>Angular Library Linking Made Easy: Paths, Workspaces, and Symlinks</h1>
<p>Managing local libraries and path references in Angular projects has evolved significantly with the introduction of the new Angular application builder. What once required manual path mappings, fragile symlinks, and <code>node_modules</code> references is now more structured, predictable, and aligned with modern TypeScript and workspace practices. This guide walks through how path mapping works, how it has changed, and the best ways to link and manage your local libraries in brand new Angular ecosystem.</p>
<h3>Understanding TypeScript Path Mapping</h3>
<p>Path aliases is a powerful feature in TypeScript that helps developers simplify and organize their import statements. Instead of dealing with long and error-prone relative paths like <code>../../../components/button</code>, you can define a clear and descriptive alias that points directly to a specific directory or module.</p>
<p>This configuration is managed through the <code>paths</code> property in the TypeScript configuration file (<code>tsconfig.json</code>), allowing you to map custom names to local folders or compiled outputs. For example:</p>
<pre><code class="language-json">// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@my-package&quot;: [&quot;./dist/my-package&quot;],
      &quot;@my-second-package&quot;: [&quot;./projects/my-second-package/src/public-api.ts&quot;]
    }
  }
}
</code></pre>
<p>In this setup, <code>@my-package</code> serves as a shorthand reference to your locally built library. Once configured, you can import modules using <code>@my-package</code> instead of long relative paths, which greatly improves readability and maintainability across large projects.</p>
<p>When working with multiple subdirectories or a more complex folder structure, you can also use wildcards to create flexible and dynamic mappings. This pattern is especially useful for modular libraries or mono-repos that contain multiple sub-packages:</p>
<pre><code class="language-json">// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@my-package/*&quot;: [&quot;./dist/my-package/*&quot;]
    }
  }
}
</code></pre>
<p>With this approach, imports like <code>@my-package/utils</code> or <code>@my-package/components/button</code> will automatically resolve to the corresponding directories in your build output. This makes your codebase more maintainable, portable, and consistent. This is useful especially when collaborating across teams or working with multiple libraries in the same workspace.</p>
<hr />
<h3>Step-by-Step Examples of Path Configuration</h3>
<p>As this example provides a glimpse for the path mapping, this is not the only way for the aliases. Here are the other ways to utilize this feature.</p>
<ol>
<li><p><strong>Using <code>package.json</code> Exports for Library Mapping</strong></p>
<p>When developing internal libraries within a mono-repo, another option is to use the <code>exports</code> field in each library’s <code>package.json</code></p>
<p>This allows Node and modern bundlers to resolve imports cleanly when consuming the library, without depending solely on TypeScript configuration.</p>
<pre><code class="language-json">// dist/my-lib/package.json
{
  &quot;name&quot;: &quot;@my-org/my-lib&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: &quot;./index.js&quot;,
    &quot;./utils&quot;: &quot;./utils/index.ts&quot;
  }
}
</code></pre>
<pre><code class="language-tsx">import { formatDate } from &quot;@my-org/my-lib/utils&quot;;
</code></pre>
<p>This approach becomes especially powerful when publishing your libraries or integrating them into larger Angular mono-repos. Because, it aligns both runtime (Node) and compile-time (TypeScript) resolution.</p>
</li>
<li><p><strong>Linking Local Libraries via Symlinks</strong></p>
<p>If you want to use a local library that is not yet published to npm, you can create a symbolic link between your library’s <code>dist</code> output and your consuming app.</p>
<p>This is useful when testing or developing multiple packages in parallel.</p>
<p>You can create a symlink using npm or yarn:</p>
<pre><code class="language-bash"># Inside your library folder
npm link

# Inside your consuming app
npm link @my-org/my-lib
</code></pre>
<p>This effectively tells Node to resolve <code>@my-org/my-lib</code> from your local file system instead of the npm registry.</p>
<p>However, note that symlinks can sometimes lead to path resolution issues with certain Angular build configurations, especially before the new application builder. With the latest builder improvements, this approach is becoming more stable and predictable.</p>
</li>
<li><p><strong>Combining Path Mapping with Workspace Configuration</strong></p>
<p>In a structured Angular workspace, especially one created with <strong>Nx</strong> or <strong>Angular CLI</strong> using multiple projects, you can combine the approaches above.</p>
<p>For instance, your <code>tsconfig.base.json</code> can define local references for in-repo libraries, while each library’s <code>package.json</code> provides external mappings for reuse outside the workspace.</p>
<p>This hybrid setup ensures that:</p>
<ul>
<li>The workspace remains easy to navigate and refactor locally.</li>
<li>External consumers (or CI builds) can still resolve imports correctly once libraries are built.</li>
</ul>
<p>For larger Angular projects or mono-repos, <strong>Workspaces</strong> (supported by both <strong>Yarn</strong> and <strong>npm</strong>) offer a clean way to manage multiple local packages within the same repository. Workspaces automatically link internal libraries together, so you can reference them by name instead of using manual <code>file:</code> paths or complex TypeScript aliases. This approach keeps dependencies consistent, simplifies cross-project development, and scales well for enterprise or multi-package setups.</p>
</li>
</ol>
<p>Each of these methods has its strengths:</p>
<ul>
<li><strong>TypeScript paths:</strong> This is great for local development and quick imports.</li>
<li><strong><code>package.json</code> exports:</strong> This is ideal for libraries meant to be distributed.</li>
<li><strong>Symlinks:</strong> These are convenient for local testing between projects.</li>
</ul>
<p>Choosing the right one, or even combining them depends on the scale of your project and whether you are building internal libraries, or a full mono-repo setup.</p>
<hr />
<h3>How Path References Worked Before the New Angular Application Builder</h3>
<p>Angular used to support path aliases to the locally installed packages by referencing to the <code>node_modules</code> folder like this:</p>
<pre><code class="language-json">// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@angular/*&quot;: [&quot;./node_modules/@angular/*&quot;]
    }
  }
}
</code></pre>
<p>However, this approach is not recommended, hence not supported, by the TypeScript. You can find detailed guidance on this topic in the TypeScript documentation, which notes that paths should not reference mono-repo packages or those inside <strong>node_modules</strong>: <a href="https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages">Paths should not point to monorepo packages or node_modules packages</a>.</p>
<p>Giving a real life example would explain the situation better. Suppose that you have such structure:</p>
<ul>
<li><p>Amain angular app that consumes several npm dependencies and holds registered local paths that reference to another library locally like this:</p>
<pre><code class="language-json">// angular/tsconfig.json
{
  &quot;compileOnSave&quot;: false,
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@abp/ng.identity&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/src/public-api.ts&quot;
      ],
      &quot;@abp/ng.identity/config&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/config/src/public-api.ts&quot;
      ],
      &quot;@abp/ng.identity/proxy&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts&quot;
      ]
    }
  }
}
</code></pre>
<p>This simply references to this package physically https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/identity</p>
</li>
<li><p>This library is also using these dependencies</p>
<pre><code class="language-json">// npm/ng-packs/packages/identity/package.json
{
  &quot;name&quot;: &quot;@abp/ng.identity&quot;,
  &quot;version&quot;: &quot;10.0.0-rc.1&quot;,
  &quot;homepage&quot;: &quot;https://abp.io&quot;,
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;https://github.com/abpframework/abp.git&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@abp/ng.components&quot;: &quot;~10.0.0-rc.1&quot;,
    &quot;@abp/ng.permission-management&quot;: &quot;~10.0.0-rc.1&quot;,
    &quot;@abp/ng.theme.shared&quot;: &quot;~10.0.0-rc.1&quot;,
    &quot;tslib&quot;: &quot;^2.0.0&quot;
  },
  &quot;publishConfig&quot;: {
    &quot;access&quot;: &quot;public&quot;
  }
}
</code></pre>
<p>As these libraries also have their own dependencies, the identity package needs to consume them in itself. Before the <a href="https://angular.dev/tools/cli/build-system-migration">application builder migration</a>, you could register the path configuration like this</p>
<pre><code class="language-json">// angular/tsconfig.json
{
  &quot;compileOnSave&quot;: false,
  &quot;compilerOptions&quot;: {
    &quot;paths&quot;: {
      &quot;@angular/*&quot;: [&quot;node_modules/@angular/*&quot;],
      &quot;@abp/*&quot;: [&quot;node_modules/@abp/*&quot;],
      &quot;@swimlane/*&quot;: [&quot;node_modules/@swimlane/*&quot;],
      &quot;@ngx-validate/core&quot;: [&quot;node_modules/@ngx-validate/core&quot;],
      &quot;@ng-bootstrap/ng-bootstrap&quot;: [
        &quot;node_modules/@ng-bootstrap/ng-bootstrap&quot;
      ],
      &quot;@abp/ng.identity&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/src/public-api.ts&quot;
      ],
      &quot;@abp/ng.identity/config&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/config/src/public-api.ts&quot;
      ],
      &quot;@abp/ng.identity/proxy&quot;: [
        &quot;../modules/Volo.Abp.Identity/angular/projects/identity/proxy/src/public-api.ts&quot;
      ]
    }
  }
}
</code></pre>
<p>However, the latest builder forces more strict rules. So, it does not resolve the paths that reference to the <code>node_modules</code> causing a common DI error as mentioned here:</p>
<ul>
<li>https://github.com/angular/angular-cli/issues/31395</li>
<li>https://github.com/angular/angular-cli/issues/26901</li>
<li>https://github.com/angular/angular-cli/issues/27176</li>
</ul>
</li>
</ul>
<p>In this case, we recommend using a symlink script. You can reach them through this example application: <a href="https://github.com/sumeyyeKurtulus/AbpPathReferenceExamples">🔗 Angular Sample Path Reference</a></p>
<p>These scripts help you share dependencies from the main Angular app to local library projects via symlinks:</p>
<ul>
<li><code>symlink-config.ps1</code> centralizes which library directories to touch (e.g., ../../modules/Volo.Abp.Identity/angular/projects/identity) and which packages to link (e.g., @angular, @abp, rxjs)</li>
<li><code>setup-symlinks.ps1</code> reads that config and, for each library, creates a <code>node_modules</code> folder if needed and symlinks only the listed packages from the <code>node_modules</code> of the app to avoid duplicate installs</li>
<li><code>remove-symlinks.ps1</code> cleans up by deleting those library <code>node_modules</code> directories so they can use their own local deps again</li>
<li>In <code>angular/package.json</code>, the <code>symlinks:setup</code> and <code>symlinks:remove</code> npm scripts simply run those two PowerShell scripts so you can execute them conveniently with your package manager.</li>
</ul>
<hr />
<h3>Best Practices and Recommendations</h3>
<p>As we have explained each way of path mapping, this part of the article aims to summarize the best practices. Here are the points you need to consider:</p>
<ul>
<li>Prefer <strong>workspace references</strong> for large projects and mono-repos.</li>
<li>Use <strong>TypeScript path aliases</strong> only for local development convenience.</li>
<li>Strictly avoid referencing <code>node_modules</code> directly; let the Angular builder manage package resolution.</li>
<li>Maintain <strong>consistent library structures</strong> with clear <code>package.json</code> exports for reusable libraries.</li>
<li>Automate <strong>symlink creation/removal</strong> if needed to reduce manual errors.</li>
</ul>
<p>Here is the list of common pitfalls and how you could troubleshoot them:</p>
<ul>
<li><strong>DI errors after path configurations for typescript config</strong>: Ensure that only one copy of each library is resolved. Avoid duplicate modules by checking <code>node_modules</code> and symlinks.</li>
<li><strong>IDE not recognizing aliases</strong>: Confirm that <code>tsconfig.json</code> or <code>tsconfig.base.json</code> includes the correct <code>paths</code> configuration and that your IDE is using the correct tsconfig.</li>
<li><strong>Build errors with old paths</strong>: Migrate paths pointing to <code>node_modules</code> to either workspace references or local library paths.</li>
<li><strong>Symlink issues in CI/CD</strong>: Use automated scripts to create/remove symlinks consistently; do not rely on manual linking.</li>
<li><strong>Module resolution conflicts</strong>: Check library dependencies for mismatched versions and align them using a package manager workspace strategy.</li>
</ul>
<p>As Angular’s build system continues to mature, developers are encouraged to move away from outdated path configurations and manual symlink setups. By embracing workspace references, consistent library exports, and TypeScript path mapping, teams can build scalable, maintainable applications without wrestling with complex import paths or dependency conflicts. With the right configuration, local development becomes faster, cleaner, and far more reliable.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1d0324-71be-afc1-bb2f-a056660a2669" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1d0324-71be-afc1-bb2f-a056660a2669" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/building-scalable-angular-apps-with-reusable-ui-components-b9npiff3</guid>
      <link>https://abp.io/community/posts/building-scalable-angular-apps-with-reusable-ui-components-b9npiff3</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>application-development</category>
      <category>modularity</category>
      <category>best-practices</category>
      <category>frontend</category>
      <title>Building Scalable Angular Apps with Reusable UI Components</title>
      <description>The purpose of this article to explain how to build scalable Angular applications by focusing on reusability through standalone components, signals, and shared libraries. It emphasizes breaking large components into smaller, focused ones that encapsulate their own logic and styles, making them easier to reuse and maintain. It also highlights best practices while encouraging the gradual creation of shared libraries to centralize common UI elements and ensure long-term maintainability.</description>
      <pubDate>Tue, 07 Oct 2025 11:10:46 Z</pubDate>
      <a10:updated>2026-10-05T08:52:39Z</a10:updated>
      <content:encoded><![CDATA[<h1>Building Scalable Angular Apps with Reusable UI Components</h1>
<p>Frontend development keeps evolving at an incredible pace, and with every new update, our implementation standards improve as well. But even as tools and frameworks change, the core principles stay the same, and one of the most important is reusability.</p>
<p>Reusability means building components and utilities that can be used in multiple places instead of using the same logic repeatedly. This approach not only saves time but also keeps your code clean, consistent, and easier to maintain as your project grows.</p>
<p>Angular fully embraces this idea by offering modern features like <strong>standalone components</strong>, <strong>signals</strong>, <strong>hybrid rendering</strong>, and <strong>component-level lazy loading</strong>.</p>
<p>In this article, we will explore how these features make it easier to build reusable UI components. We will also look at how to style them and organize them into shared libraries for scalable, long-term development.</p>
<hr />
<h2>🧩 Breaking Down Components for True Reusability</h2>
<p>The first approach to make an Angular component reusable is to use standalone components. As this feature has been supported for a long time, it is now the default behavior for the latest Angular versions. Keeping that in mind, we can ensure reusability by separating a big component into smaller ones to make the small pieces usable across the application.</p>
<p>Here is a quick example:</p>
<p>Imagine you start with a single <code>UserProfileComponent</code> that does everything including displaying user info, recent posts, a list of friends, and even handling profile editing.</p>
<pre><code class="language-ts">// 📖 Compact user profile component
import { Component } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-user-profile&quot;,
  template: `
    &lt;section class=&quot;profile&quot;&gt;
      &lt;div class=&quot;header&quot;&gt;
        &lt;img [src]=&quot;user.avatar&quot; alt=&quot;User avatar&quot; /&gt;
        &lt;h2&gt;{{ user.name }}&lt;/h2&gt;
        &lt;button (click)=&quot;editProfile()&quot;&gt;Edit&lt;/button&gt;
      &lt;/div&gt;

      &lt;div class=&quot;posts&quot;&gt;
        &lt;h3&gt;Recent Posts&lt;/h3&gt;
        &lt;ul&gt;
          @for (post of user.posts; track post) {
          &lt;li&gt;{{ post }}&lt;/li&gt;
          }
        &lt;/ul&gt;
      &lt;/div&gt;

      &lt;div class=&quot;friends&quot;&gt;
        &lt;h3&gt;Friends&lt;/h3&gt;
        &lt;ul&gt;
          @for (friend of user.friends; track friend) {
          &lt;li&gt;{{ friend }}&lt;/li&gt;
          }
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  `,
})
export class UserProfileComponent {
  user = {
    name: &quot;Jane Doe&quot;,
    avatar: &quot;/assets/avatar.png&quot;,
    posts: [&quot;Angular Tips&quot;, &quot;Reusable Components FTW!&quot;],
    friends: [&quot;John&quot;, &quot;Mary&quot;, &quot;Steve&quot;],
  };

  editProfile() {
    console.log(&quot;Editing profile...&quot;);
  }
}
</code></pre>
<p>Instead of this, you can create small components like these:</p>
<ul>
<li><code>user-avatar.component.ts</code></li>
<li><code>user-posts.component.ts</code></li>
<li><code>user-friends.component.ts</code></li>
</ul>
<pre><code class="language-ts">// 🧩 user-avatar.component.ts
import { Component, input } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-user-avatar&quot;,
  template: `
    &lt;div class=&quot;user-avatar&quot;&gt;
      &lt;img [src]=&quot;avatar()&quot; alt=&quot;User avatar&quot; /&gt;
      &lt;h2&gt;{{ name() }}&lt;/h2&gt;
    &lt;/div&gt;
  `,
})
export class UserAvatarComponent {
  name = input.required&lt;string&gt;();
  avatar = input.required&lt;string&gt;();
}
</code></pre>
<pre><code class="language-ts">// 🧩 user-posts.component.ts
import { Component, input } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-user-posts&quot;,
  template: `
    &lt;div class=&quot;user-posts&quot;&gt;
      &lt;h3&gt;Recent Posts&lt;/h3&gt;
      &lt;ul&gt;
        @for (post of posts(); track post) {
        &lt;li&gt;{{ post }}&lt;/li&gt;
        }
      &lt;/ul&gt;
    &lt;/div&gt;
  `,
})
export class UserPostsComponent {
  posts = input&lt;string[]&gt;([]);
}
</code></pre>
<pre><code class="language-ts">// 🧩 user-friends.component.ts
import { Component, input, output } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-user-friends&quot;,
  template: `
    &lt;div class=&quot;user-friends&quot;&gt;
      &lt;h3&gt;Friends&lt;/h3&gt;
      &lt;ul&gt;
        @for (friend of friends(); track friend) {
        &lt;li (click)=&quot;selectFriend(friend)&quot;&gt;{{ friend }}&lt;/li&gt;
        }
      &lt;/ul&gt;
    &lt;/div&gt;
  `,
})
export class UserFriendsComponent {
  friends = input&lt;string[]&gt;([]);
  friendSelected = output&lt;string&gt;();

  selectFriend(friend: string) {
    this.friendSelected.emit(friend);
  }
}
</code></pre>
<p>Then, you can use them in a container component like this</p>
<pre><code class="language-ts">// 🧩 new user profile components that uses other user components
import { Component } from &quot;@angular/core&quot;;
import { signal } from &quot;@angular/core&quot;;
import { UserAvatarComponent } from &quot;./user-avatar.component&quot;;
import { UserPostsComponent } from &quot;./user-posts.component&quot;;
import { UserFriendsComponent } from &quot;./user-friends.component&quot;;

@Component({
  selector: &quot;app-user-profile&quot;,
  imports: [UserAvatarComponent, UserPostsComponent, UserFriendsComponent],
  template: `
    &lt;section class=&quot;profile&quot;&gt;
      &lt;app-user-avatar [name]=&quot;user().name&quot; [avatar]=&quot;user().avatar&quot; /&gt;
      &lt;app-user-posts [posts]=&quot;user().posts&quot; /&gt;
      &lt;app-user-friends
        [friends]=&quot;user().friends&quot;
        (friendSelected)=&quot;onFriendSelected($event)&quot;
      /&gt;
    &lt;/section&gt;
  `,
})
export class UserProfileComponent {
  user = signal({
    name: &quot;Jane Doe&quot;,
    avatar: &quot;/assets/avatar.png&quot;,
    posts: [&quot;Angular Tips&quot;, &quot;Reusable Components FTW!&quot;],
    friends: [&quot;John&quot;, &quot;Mary&quot;, &quot;Steve&quot;],
  });

  onFriendSelected(friend: string) {
    console.log(`Selected friend: ${friend}`);
  }
}
</code></pre>
<p>The most common problem of creating such components is over-creating new elements when you actually do not need them. So, it is a design decision that needs to be carefully taken while building the application. If misused, it can lead to:</p>
<ul>
<li>a management nightmare</li>
<li>unnecessary lifecycle hook complexity</li>
<li>extra indirect data flow (makes debugging harder)</li>
</ul>
<p>Nevertheless, this makes the app more scalable and maintainable if correctly used. Such structure will provide:</p>
<ul>
<li>a clear separation of concerns as each component will maintain decided tasks</li>
<li>faster feature development</li>
<li>shared libraries or elements across the application</li>
</ul>
<hr />
<h2>🚀 Why Standalone Components Matter</h2>
<p>As Angular has announced standalone components starting from version 17, they have been gradually developing features that support reusability. This important feature brings a great migration for components, directives, and pipes.</p>
<p>Since it allows these elements to be used directly inside an <code>imports</code> array rather than through a module structure, it reinforces reusability patterns and simplifies management.</p>
<p>Back in the module-based structure, we used to create these components and declare them in modules. This still offers some reusability, as we can import the modules where needed. However, standalone components can be consumed both by other standalone components and modules. For this reason, migrating from the module-based structure to a fully standalone architecture brings many benefits for this concern.</p>
<hr />
<h2>🧠 Designing Components That Scale and Reuse Well</h2>
<p>The first point you need to consider here is to encapsulate and isolate logic.</p>
<p>For example:</p>
<ol>
<li><p>This counter component isolates the concept of incrementing/decrementing so the parent component will not take care of this logic except showing the result.</p>
<pre><code class="language-ts">import { Component, signal } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-counter&quot;,
  template: `
    &lt;button (click)=&quot;decrement()&quot;&gt;-&lt;/button&gt;
    &lt;span&gt;{{ count() }}&lt;/span&gt;
    &lt;button (click)=&quot;increment()&quot;&gt;+&lt;/button&gt;
  `,
})
export class CounterComponent {
  private count = signal(0); // internal state

  increment() {
    this.count.update((v) =&gt; v + 1);
  }
  decrement() {
    this.count.update((v) =&gt; v - 1);
  }
}
</code></pre>
</li>
<li><p>This component isolates the styles and makes the badge reusable. Styles in this component will not leak out to others, and global styles will not affect it.</p>
<pre><code class="language-ts">import { Component, ViewEncapsulation } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-badge&quot;,
  template: `&lt;span class=&quot;badge&quot;&gt;{{ label }}&lt;/span&gt;`,
  styles: [
    `
      .badge {
        background: #007bff;
        color: white;
        padding: 4px 8px;
        border-radius: 4px;
      }
    `,
  ],
  encapsulation: ViewEncapsulation.Emulated, // default; isolates CSS
})
export class BadgeComponent {
  label = &quot;New&quot;;
}
</code></pre>
</li>
<li><p>The search component below is a very common example since it handles a business logic exposing simple inputs/outputs</p>
<pre><code class="language-ts">import { Component, input, output } from &quot;@angular/core&quot;;

@Component({
  selector: &quot;app-search-box&quot;,
  template: `
    &lt;input
      type=&quot;text&quot;
      [value]=&quot;query()&quot;
      (input)=&quot;onChange($event)&quot;
      placeholder=&quot;Search...&quot;
    /&gt;
  `,
})
export class SearchBoxComponent {
  query = input&lt;string&gt;(&quot;&quot;);
  changed = output&lt;string&gt;();

  onChange(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.changed.emit(value);
  }
}
</code></pre>
</li>
</ol>
<p>Encapsulation ensures that each component manages its own logic without leaking details to the outside. By keeping behavior self-contained, components become easier to understand, test, and reuse. This isolation prevents unexpected side effects, keeps your UI predictable, and allows each component to evolve independently as your application grows.</p>
<p>At this point, we can also briefly mention smart and dumb components. Smart components handle business logic, while dumb components take care of displaying data and emitting user actions.</p>
<p>This separation keeps your UI structure scalable. Smart components can change how data is loaded or handled without affecting presentation components, and dumb components can be reused anywhere since they just rely on inputs and outputs.</p>
<pre><code class="language-ts">// smart component (container)
@Component({
  selector: &quot;app-user-profile&quot;,
  imports: [UserCardComponent],
  template: `&lt;app-user-card [user]=&quot;user()&quot; (select)=&quot;onSelect($event)&quot; /&gt;`,
})
export class UserProfileComponent {
  user = signal({ name: &quot;Jane&quot;, role: &quot;Admin&quot; });

  onSelect(user: any) {
    console.log(&quot;Selected user:&quot;, user);
  }
}

// dumb component (presentation)
@Component({
  selector: &quot;app-user-card&quot;,
  standalone: true,
  template: `
    &lt;div (click)=&quot;select.emit(user())&quot; class=&quot;card&quot;&gt;
      &lt;h3&gt;{{ user().name }}&lt;/h3&gt;
      &lt;p&gt;{{ user().role }}&lt;/p&gt;
    &lt;/div&gt;
  `,
})
export class UserCardComponent {
  user = input.required&lt;{ name: string; role: string }&gt;();
  select = output&lt;{ name: string; role: string }&gt;();
}
</code></pre>
<hr />
<h2>🔁 Reusing Components Across the Application</h2>
<p>As there are many ways of reusing a component in the project, we will go over a real-life example.</p>
<p>Here are two very common ABP components that can be reused anywhere in the app:</p>
<pre><code class="language-ts">//...
import { ABP } from &quot;@abp/ng.core&quot;;

@Component({
  selector: &quot;abp-button&quot;,
  template: `
    &lt;button
      #button
      [id]=&quot;buttonId&quot;
      [attr.type]=&quot;buttonType&quot;
      [attr.form]=&quot;formName&quot;
      [ngClass]=&quot;buttonClass&quot;
      [disabled]=&quot;loading || disabled&quot;
      (click.stop)=&quot;click.next($event); abpClick.next($event)&quot;
      (focus)=&quot;focus.next($event); abpFocus.next($event)&quot;
      (blur)=&quot;blur.next($event); abpBlur.next($event)&quot;
    &gt;
      &lt;i [ngClass]=&quot;icon&quot; class=&quot;me-1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/i
      &gt;&lt;ng-content&gt;&lt;/ng-content&gt;
    &lt;/button&gt;
  `,
  imports: [NgClass],
})
export class ButtonComponent implements OnInit {
  private renderer = inject(Renderer2);

  @Input()
  buttonId = &quot;&quot;;

  @Input()
  buttonClass = &quot;btn btn-primary&quot;;

  @Input()
  buttonType = &quot;button&quot;;

  @Input()
  formName?: string = undefined;

  @Input()
  iconClass?: string;

  @Input()
  loading = false;

  @Input()
  disabled: boolean | undefined = false;

  @Input()
  attributes?: ABP.Dictionary&lt;string&gt;;

  @Output() readonly click = new EventEmitter&lt;MouseEvent&gt;();

  @Output() readonly focus = new EventEmitter&lt;FocusEvent&gt;();

  @Output() readonly blur = new EventEmitter&lt;FocusEvent&gt;();

  @Output() readonly abpClick = new EventEmitter&lt;MouseEvent&gt;();

  @Output() readonly abpFocus = new EventEmitter&lt;FocusEvent&gt;();

  @Output() readonly abpBlur = new EventEmitter&lt;FocusEvent&gt;();

  @ViewChild(&quot;button&quot;, { static: true })
  buttonRef!: ElementRef&lt;HTMLButtonElement&gt;;

  get icon(): string {
    return `${
      this.loading ? &quot;fa fa-spinner fa-spin&quot; : this.iconClass || &quot;d-none&quot;
    }`;
  }

  ngOnInit() {
    if (this.attributes) {
      Object.keys(this.attributes).forEach((key) =&gt; {
        if (this.attributes?.[key]) {
          this.renderer.setAttribute(
            this.buttonRef.nativeElement,
            key,
            this.attributes[key]
          );
        }
      });
    }
  }
}
</code></pre>
<p>This button component can be used by simply importing the <code>ButtonComponent</code> and using the <code>&lt;abp-button /&gt;</code> tag.</p>
<p>You can reach the source code <a href="https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts">here</a>.</p>
<p>This modal component is also commonly used. The source code is <a href="https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts">here</a>.</p>
<pre><code class="language-ts">//...
export type ModalSize = &quot;sm&quot; | &quot;md&quot; | &quot;lg&quot; | &quot;xl&quot;;

@Component({
  selector: &quot;abp-modal&quot;,
  templateUrl: &quot;./modal.component.html&quot;,
  styleUrls: [&quot;./modal.component.scss&quot;],
  providers: [SubscriptionService],
  imports: [NgTemplateOutlet],
})
export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
  protected readonly confirmationService = inject(ConfirmationService);
  protected readonly modal = inject(NgbModal);
  protected readonly modalRefService = inject(ModalRefService);
  protected readonly suppressUnsavedChangesWarningToken = inject(
    SUPPRESS_UNSAVED_CHANGES_WARNING,
    {
      optional: true,
    }
  );
  protected readonly destroyRef = inject(DestroyRef);
  private document = inject(DOCUMENT);

  visible = model&lt;boolean&gt;(false);

  busy = input(false, {
    transform: (value: boolean) =&gt; {
      if (this.abpSubmit() &amp;&amp; this.abpSubmit() instanceof ButtonComponent) {
        this.abpSubmit().loading = value;
      }
      return value;
    },
  });

  options = input&lt;NgbModalOptions&gt;({ keyboard: true });

  suppressUnsavedChangesWarning = input(
    this.suppressUnsavedChangesWarningToken
  );

  modalContent = viewChild&lt;TemplateRef&lt;any&gt;&gt;(&quot;modalContent&quot;);

  abpHeader = contentChild&lt;TemplateRef&lt;any&gt;&gt;(&quot;abpHeader&quot;);

  abpBody = contentChild&lt;TemplateRef&lt;any&gt;&gt;(&quot;abpBody&quot;);

  abpFooter = contentChild&lt;TemplateRef&lt;any&gt;&gt;(&quot;abpFooter&quot;);

  abpSubmit = contentChild(ButtonComponent, { read: ButtonComponent });

  readonly init = output();

  readonly appear = output();

  readonly disappear = output();

  modalRef!: NgbModalRef;

  isConfirmationOpen = false;

  modalIdentifier = `modal-${uuid()}`;

  get modalWindowRef() {
    return this.document.querySelector(
      `ngb-modal-window.${this.modalIdentifier}`
    );
  }

  get isFormDirty(): boolean {
    return Boolean(this.modalWindowRef?.querySelector(&quot;.ng-dirty&quot;));
  }

  constructor() {
    effect(() =&gt; {
      this.toggle(this.visible());
    });
  }

  ngOnInit(): void {
    this.modalRefService.register(this);
  }

  dismiss(mode: ModalDismissMode) {
    switch (mode) {
      case &quot;hard&quot;:
        this.visible.set(false);
        break;
      case &quot;soft&quot;:
        this.close();
        break;
      default:
        break;
    }
  }

  protected toggle(value: boolean) {
    this.visible.set(value);

    if (!value) {
      this.modalRef?.dismiss();
      this.disappear.emit();
      return;
    }

    setTimeout(() =&gt; this.listen(), 0);
    this.modalRef = this.modal.open(this.modalContent(), {
      size: &quot;md&quot;,
      centered: false,
      keyboard: false,
      scrollable: true,
      beforeDismiss: () =&gt; {
        if (!this.visible()) return true;

        this.close();
        return !this.visible();
      },
      ...this.options(),
      windowClass: `${this.options().windowClass || &quot;&quot;} ${
        this.modalIdentifier
      }`,
    });

    this.appear.emit();
  }

  ngOnDestroy(): void {
    this.modalRefService.unregister(this);
    this.toggle(false);
  }

  close() {
    if (this.busy()) return;

    if (this.isFormDirty &amp;&amp; !this.suppressUnsavedChangesWarning()) {
      if (this.isConfirmationOpen) return;

      this.isConfirmationOpen = true;
      this.confirmationService
        .warn(
          &quot;AbpUi::AreYouSureYouWantToCancelEditingWarningMessage&quot;,
          &quot;AbpUi::AreYouSure&quot;,
          {
            dismissible: false,
          }
        )
        .subscribe((status: Confirmation.Status) =&gt; {
          this.isConfirmationOpen = false;
          if (status === Confirmation.Status.confirm) {
            this.visible.set(false);
          }
        });
    } else {
      this.visible.set(false);
    }
  }

  listen() {
    if (this.modalWindowRef) {
      fromEvent&lt;KeyboardEvent&gt;(this.modalWindowRef, &quot;keyup&quot;)
        .pipe(
          takeUntilDestroyed(this.destroyRef),
          debounceTime(150),
          filter(
            (key: KeyboardEvent) =&gt;
              key &amp;&amp; key.key === &quot;Escape&quot; &amp;&amp; this.options().keyboard
          )
        )
        .subscribe(() =&gt; this.close());
    }

    fromEvent(window, &quot;beforeunload&quot;)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((event) =&gt; {
        if (this.isFormDirty &amp;&amp; !this.suppressUnsavedChangesWarning()) {
          event.preventDefault();
        }
      });

    this.init.emit();
  }
}
</code></pre>
<p>This concept differs slightly from the others mentioned above since these components are introduced within a library called <code>theme-shared</code>, which you can explore <a href="https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/theme-shared">here</a>.</p>
<p>Using <strong>shared libraries</strong> for such common components is one of the most effective ways to make your app modular and maintainable. By grouping frequently used elements into a dedicated library, you create a single source of truth for your UI and logic.</p>
<p>However, over-creating or prematurely abstracting small pieces of logic into separate libraries can lead to unnecessary complexity and dependency management overhead. When every feature has its own “mini-library,” updates and debugging become scattered and difficult to coordinate.</p>
<p>The key is to extract shared functionality only when it is proven to be reused across multiple contexts. Start small, let patterns emerge naturally, and then move them into a shared library when the benefits of reusability outweigh the maintenance cost.</p>
<hr />
<h2>⚙️ Best Practices and Common Pitfalls</h2>
<h3>✅ Best Practices</h3>
<ol>
<li><strong>Start with real reuse:</strong> Extract components only after the pattern appears in multiple places.</li>
<li><strong>Keep them focused:</strong> One clear responsibility per component—avoid “do-it-all” designs.</li>
<li><strong>Use standalone components:</strong> Simplify imports and improve independence.</li>
<li><strong>Promote through libraries:</strong> Move proven, stable components into shared libraries for wider use.</li>
</ol>
<h3>⚠️ Common Mistakes</h3>
<ol>
<li><strong>Premature abstraction:</strong> Don't create components before actual reuse.</li>
<li><strong>Too many input/output bindings:</strong> Overly generic components are hard to configure and maintain.</li>
<li><strong>Neglecting performance:</strong> Too many micro-components can hurt performance.</li>
<li><strong>Ignoring accessibility and semantics:</strong> Reusable does not mean usable—always consider ARIA roles and HTML structure.</li>
</ol>
<hr />
<h2>📚 Further Reading and References</h2>
<p>As this article has mentioned some concepts and best practices, you can explore these resources for more details:</p>
<ul>
<li><a href="https://angular.dev/guide/components">Angular Components Guide</a></li>
<li><a href="https://angular.dev/reference/migrations/standalone">Standalone Migration Guides</a>, <a href="https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z#gsc.tab=0">ABP Angular Standalone Applications</a></li>
<li><a href="https://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why/">Smart vs. Dumb Components</a></li>
<li><a href="https://angular.dev/tools/libraries">Angular Libraries Overview</a></li>
</ul>
<p>You can also check these open-source libraries for a better understanding of reusability and modularity:</p>
<ul>
<li><a href="https://github.com/angular/components">Angular Components on GitHub</a></li>
<li><a href="https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages">ABP NPM Libraries</a></li>
</ul>
<hr />
<h2>🏁 Conclusion</h2>
<p>Reusability is one of the strongest architectural foundations for scalable Angular applications. By combining <strong>standalone components</strong>, <strong>signals</strong>, <strong>encapsulated logic</strong>, and <strong>shared libraries</strong>, you can create a modular system that grows gracefully over time.</p>
<p>The goal is not just to make components reusable. It is to make them meaningful, maintainable, and consistent across your app. Build only what truly adds value, reuse intentionally, and let Angular's evolving ecosystem handle the rest.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1cd08a-b3c3-0eb4-59cf-f2d183f8c63e" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1cd08a-b3c3-0eb4-59cf-f2d183f8c63e" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/supercharge-your-angular-app-a-developers-guide-to-unlock-peak-performance-0dmu7tkr</guid>
      <link>https://abp.io/community/posts/supercharge-your-angular-app-a-developers-guide-to-unlock-peak-performance-0dmu7tkr</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>performance</category>
      <category>optimization</category>
      <title>Supercharge Your Angular App: A Developer's Guide to Unlock Peak Performance</title>
      <description>Angular keeps adding features to improve performance, from lazy loading to signals-based change detection. This article covers key techniques like lazy loading, code splitting, and advanced change detection to help cut load times and boost responsiveness.</description>
      <pubDate>Thu, 25 Sep 2025 12:05:44 Z</pubDate>
      <a10:updated>2026-10-05T07:13:28Z</a10:updated>
      <content:encoded><![CDATA[<h1>Supercharge Your Angular App: A Developer's Guide to Unlock Peak Performance</h1>
<p>Angular has consistently introduced new features to boost application performance, from lazy-loaded components to reactive change detection with signals. This article explores key optimization techniques, including <strong>lazy loading</strong>, <strong>on-demand element loading</strong>, <strong>signals-based state management</strong>, <strong>code splitting and tree shaking</strong>, and advanced <strong>change detection strategies</strong>. By understanding and implementing these practices, developers can significantly improve initial load time of the applicarion and overall responsiveness.</p>
<hr />
<h2>1. Lazy Loading Approach</h2>
<p>As the name implies, lazy loading provides the elements only when they are needed, differentiating it from eager loading. By enabling this feature for routes or modules, the bundle size gets smaller and the loading time decreases.</p>
<h3>🧘🏼‍♀️ Lazy Loaded Components</h3>
<p>The first way to integrate lazy loading into your application is to load components this way. First, you can create a component using this command <code>ng g component book</code>, and it will look like a regular standalone component:</p>
<pre><code class="language-ts">import { Component } from '@angular/core';

@Component({
  selector: 'app-book',
  imports: [],
  templateUrl: './book.html',
  styleUrl: './book.scss'
})
export class Book { }
</code></pre>
<p>Then, you can declare a routing for the component and use <code>loadComponent</code> instead of <code>component</code> to load it lazily.</p>
<pre><code class="language-ts">//app.routes.ts
import { Routes } from '@angular/router';
export const APP_ROUTES: Routes = [
  {
    path: 'book',
    loadComponent: () =&gt; import('./book/book').then(c =&gt; c.Book),
  },
];
</code></pre>
<p>Once you navigate to the books route, the component will be loaded. Lazy loading a component is a key technique for reducing the initial bundle size of your application. Instead of including code of the component in the main bundle that is downloaded when a user first visits your site, Angular creates a separate, small bundle for the Book component. This bundle is only downloaded and parsed by the browser when the user actually navigates to the <code>/book</code> route. This approach results in a faster initial load time and a more responsive user experience, as the browser does not have to process a large amount of unnecessary code upfront. The <code>import()</code> syntax is a dynamic import, which tells a module bundler like Webpack or Vite to create a separate chunk for the imported file, enabling this lazy-loading behavior.</p>
<h3>🚲 Lazy Loaded Modules</h3>
<p>If you still continue using the module-based structure, you can load the modules lazily as well. However, this approach will be marked as deprecated in the future. For this reason, we recommend a migration where you can <a href="https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z#gsc.tab=0">follow related steps</a>.</p>
<p>First, you need to create a module using such command <code>ng g module book</code>. Then, you can create a component and connect to this module using this command <code>ng g component book --module book</code>. The final result will be like this:</p>
<pre><code class="language-ts">//book.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookComponent } from './book.component';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    BookComponent
  ]
})
export class BookModule { }
</code></pre>
<pre><code class="language-ts">//book.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-book',
  imports: [],
  templateUrl: './book.component.html',
  styleUrl: './book.component.scss'
})
export class BookComponent {}
</code></pre>
<p>Then, you can declare a routing for the module and use <code>loadChildren</code> instead of <code>component</code> to load it lazily.</p>
<pre><code class="language-ts">//app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
  {
    path: 'book',
    loadChildren: () =&gt; import('./book/book.module').then(m =&gt; m.BookModule),
  },
];
@NgModule({
  imports: [RouterModule.forRoot(routes, {})],
  exports: [RouterModule],
})
export class AppRoutingModule {}
</code></pre>
<hr />
<h2>2. Code Splitting and Tree Shaking</h2>
<p>Code splitting and tree shaking are two powerful techniques that work hand-in-hand to ensure your users download and execute only the JavaScript they truly need.</p>
<h3>📘 Code Splitting</h3>
<p>Code splitting is the process of breaking a large JavaScript bundle into smaller, more manageable chunks. Instead of sending one massive file to the browser at load time, Angular delivers only what is necessary at the moment.</p>
<p>Loading components or modules lazily, using deferrable views and signals are examples of code splitting.</p>
<h3>📈 The benefits are:</h3>
<ul>
<li>Faster initial load time (reduced bundle size).</li>
<li>More efficient use of network bandwidth.</li>
<li>Scales better as applications grow in features.</li>
</ul>
<h3>📘 Tree Shaking</h3>
<p>Tree shaking is a form of dead code elimination. It analyzes your imports, detects which parts of a library or file are unused, and removes them from the final build. The metaphor comes from shaking a tree: the dead leaves fall off, leaving only what’s alive (used code).</p>
<pre><code class="language-ts">import { usefulFunction } from './helpers';
import { unusedFunction } from './helpers';

usefulFunction();
</code></pre>
<p>Here, <code>unusedFunction</code> is never called. With tree shaking, the Angular compiler ensures that this unused code won’t appear in the final production bundle.</p>
<h3>📈 The benefits are:</h3>
<ul>
<li>Reduced JavaScript size.</li>
<li>Less parsing and execution time for the browser.</li>
<li>Lower memory usage.</li>
</ul>
<p><strong>Code Splitting</strong> decides <strong>when</strong> to load code, while <strong>Tree Shaking</strong> decides <strong>what</strong> code gets included in the first place. Together, they ensure the Angular app is both lean and efficient.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/fdd71f2be791350404bc2635ad5c0cd31b431e51/docs/en/Community-Articles/2025-09-25-Supercharge-Your-Angular-App-A-Developer%27s-Guide-to-Unlocking-Peak-Performance/code-splitting-and-tree-shaking.png" alt="code-splitting-and-tree-shaking" /></p>
<hr />
<h2>3. Loading Elements on Demand using <code>@defer</code> block</h2>
<p>The aim of using this view kind is also to reduce the initial bundle size of the application. Angular documentation claims that the initial load becomes faster, and it enhances the Core Web Vitals (CVW), especially for Largest Contentful Paint (LCP) and Time to First Byte (TTFB).</p>
<p>In order to use this block, your component needs to be standalone, and you cannot re-use the component outside of the deferring block. Here is how you can use it in your template:</p>
<pre><code class="language-ts">@defer {
  &lt;large-component /&gt;
}
</code></pre>
<p>You can also propose loading, error, and placeholder states out of the box.</p>
<pre><code class="language-ts">@defer {
  &lt;large-component /&gt;
} @loading {
  &lt;img alt=&quot;loading...&quot; src=&quot;https://raw.githubusercontent.com/abpframework/abp/fdd71f2be791350404bc2635ad5c0cd31b431e51/docs/en/Community-Articles/2025-09-25-Supercharge-Your-Angular-App-A-Developer%27s-Guide-to-Unlocking-Peak-Performance/loading.gif&quot; /&gt;
} @placeholder {
  &lt;p&gt;Placeholder content&lt;/p&gt;
} @error {
  &lt;p&gt;Failed to load large component.&lt;/p&gt;
}
</code></pre>
<p>The framework also provides content loading based on triggers. These are <strong>on</strong> and <strong>when</strong>. Basically, the <strong>on</strong> property option relies on the user interaction, and the <strong>when</strong> property relies on the specified condition.</p>
<hr />
<h2>4. Using Signals for State Management</h2>
<p>Angular introduced signals as part of its reactivity model to make state management and change detection more precise and performant compared to the traditional Zone.js-based approach. Signals shift Angular from a 'check everything on every event' model to a 'react only to what changed' model. This makes applications more efficient, faster, and often simpler to reason about. Here are the benefits of signals in terms of performance optimization:</p>
<h3>✅ Reduces unnecessary work and makes faster UI updates.</h3>
<p>Traditionally, Angular relies on zone-based change detection, which means that any asynchronous event (e.g., click, HTTP request, timer) triggers a full change detection cycle across the entire component tree. Since Angular knows exactly which component property depends on which reactive value, this allows Angular to update only the affected components/elements when the signal changes, instead of re-checking everything.</p>
<h3>✅ Dependencies are tracked explicitly.</h3>
<p>A signal records its dependencies at runtime. When a computed signal or template expression reads a signal, Angular automatically tracks that dependency. If the value changes, only consumers of that signal re-run. So, the unrelated computations are prevented, and the performance is optimized.</p>
<h3>✅ The updates are synchronous and predictable.</h3>
<p>When you set a new value in a signal, all dependent computations and template updates happen immediately and predictably. So, it is easy to debug and determine the performance in that sense.</p>
<hr />
<h2>5. Change Detection</h2>
<p>Angular uses a change detection mechanism to keep the view in sync with the component state. By default, Angular uses the default change detection strategy for every component.</p>
<h3>Default Strategy Mechanism</h3>
<p>When an event happens (like a user input, HTTP response, <code>setTimeout</code>, etc.), Angular runs change detection. It starts from the root component and traverses every component in the tree to check if any bound data has changed. Angular compares the current property values with the previous ones for each component. As can be perceived, if something changed, Angular updates the DOM. Even if a change occurs in one component, Angular will still go through all components down the tree unless properly optimized.</p>
<p>The good side is that it keeps the UI of the application constant with the state no matter where the change is coming from. However, it becomes a bottleneck for large applications as all components are going to be checked.</p>
<h3>Optimizing the Default Strategy</h3>
<p>Even with the Default strategy, you can improve performance by:</p>
<ul>
<li>Breaking up elements into smaller, reusable ones, so Angular checks smaller trees.</li>
<li>Using <code>track</code> in <code>@for</code> blocks to prevent re-rendering on unchanged list items.</li>
<li>Not adding heavy computations on templates or making them into smaller ones in services.</li>
</ul>
<p>The other change detection strategy is <strong>OnPush</strong>. This will allow you to make checks when needed. By this means, Angular would check only if:</p>
<ul>
<li>an input reference is changed</li>
<li>an event is triggered</li>
<li>or you trigger it manually by using <code>markForCheck</code> utility</li>
</ul>
<h3>Manual Change Detection Triggers</h3>
<p>Angular provides these controls by <code>ChangeDetectorRef</code> class injection.</p>
<p><strong><code>markForCheck()</code></strong></p>
<p>This marks the component along its ancestors as dirty. The component will be checked in the next change detection cycle. In this example case, a component uses the OnPush strategy, and something is updated directly (e.g. a service emits a new value without changing the <code>input()</code> reference).</p>
<pre><code class="language-ts">private cdr = inject(ChangeDetectorRef);

updateData() {
  this.data.someProp = 'newValue';
  this.cdr.markForCheck();  // schedule re-check
}
</code></pre>
<p><strong><code>detectChanges()</code></strong></p>
<p>Immediately runs change detection synchronously on the component and its children. In this example, the DOM is reflected by the change immediately that could be after a dialog closing, or measuring the size of the element.</p>
<pre><code class="language-ts">private cdr = inject(ChangeDetectorRef);

updateData() {
  this.data.someProp = 'newValue';
  this.cdr.detectChanges();  // run CD immediately
}
</code></pre>
<p><strong>In short:</strong></p>
<ul>
<li>Use <code>markForCheck()</code> when performance is the key and you want to stay within the normal change detection cycle of Angular.</li>
<li>Use <code>detectChanges()</code> when you absolutely need the update immediately after the change occurs.</li>
</ul>
<hr />
<h2>Conclusion</h2>
<p>By implementing these strategies, developers can build Angular applications that are not only feature-rich but also fast and efficient. <strong>Lazy loading</strong>, <strong>code splitting</strong>, and <strong>tree shaking</strong> work together to reduce initial load times, while <strong>signals</strong> and optimized <strong>change detection</strong> ensure the application remains responsive and performant as it grows in complexity. Embracing these modern Angular features is crucial for delivering a high-quality user experience and maintaining a scalable codebase.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c92f0-b932-7f69-d264-433b9b727263" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c92f0-b932-7f69-d264-433b9b727263" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-now-supports-angular-standalone-applications-zzi2rr2z</guid>
      <link>https://abp.io/community/posts/abp-now-supports-angular-standalone-applications-zzi2rr2z</link>
      <a10:author>
        <a10:name>sumeyye.kurtulus</a10:name>
        <a10:uri>https://abp.io/community/members/sumeyye.kurtulus</a10:uri>
      </a10:author>
      <category>angular</category>
      <category>abp</category>
      <category>migration</category>
      <category>updates</category>
      <category>abpframework</category>
      <title>ABP Now Supports Angular Standalone Applications</title>
      <description>With our latest updates, ABP now supports standalone structure of Angular. Read on to learn how to generate a new standalone application, get step-by-step instructions for migrating your current projects, and discover the benefits of moving away from the traditional module-based architecture.

</description>
      <pubDate>Wed, 17 Sep 2025 07:01:29 Z</pubDate>
      <a10:updated>2026-10-05T09:32:47Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP Now Supports Angular Standalone Applications</h1>
<p>We are excited to announce that <strong>ABP now supports Angular’s standalone component structure</strong> in the latest Studio update. This article walks you through how to generate a standalone application, outlines the migration steps, and highlights the benefits of this shift over traditional module-based architecture.</p>
<hr />
<h2>Why Standalone?</h2>
<p>Angular's standalone component architecture, which is introduced in version 14 and made default in version 19, is a major leap forward for Angular development. Here is why it matters:</p>
<h3>🔧 Simplified Project Structure</h3>
<p>Standalone components eliminate the need for <code>NgModule</code> wrappers. This leads to:</p>
<ul>
<li>Fewer files to manage</li>
<li>Cleaner folder organization</li>
<li>Reduced boilerplate</li>
</ul>
<p>Navigating and understanding your codebase becomes easier for everyone on your team.</p>
<h3>🚀 Faster Bootstrapping</h3>
<p>Standalone apps simplify app initialization:</p>
<pre><code class="language-ts">bootstrapApplication(AppComponent, appConfig);
</code></pre>
<p>This avoids the need for <code>AppModule</code> and speeds up startup times.</p>
<h3>📦 Smaller Bundle Sizes</h3>
<p>Since components declare their own dependencies, Angular can more effectively tree-shake unused code. Result? Smaller bundle sizes and faster load times.</p>
<h3>🧪 Easier Testing &amp; Reusability</h3>
<p>Standalone components are self-contained. They declare their dependencies within the <code>imports</code> array, making them:</p>
<ul>
<li>Easier to test in isolation</li>
<li>Easier to reuse in different contexts</li>
</ul>
<h3>🧠 Clearer Dependency Management</h3>
<p>Standalone components explicitly define what they need. No more hidden dependencies buried in shared modules.</p>
<h3>🔄 Gradual Adoption</h3>
<p>You can mix and match standalone and module-based components. This allows for <strong>incremental migration</strong>, reducing risk in larger codebases. Here is the related document for the <a href="https://angular.dev/reference/migrations/standalone">standalone migration</a>.</p>
<hr />
<h2>Getting Started: Creating a Standalone Angular App</h2>
<p>Angular CLI makes it easy to start:</p>
<pre><code class="language-bash">ng new my-app
</code></pre>
<p>With Angular 19, new apps follow this bootstrapping model:</p>
<pre><code class="language-ts">// main.ts
import { bootstrapApplication } from &quot;@angular/platform-browser&quot;;
import { appConfig } from &quot;./app/app.config&quot;;
import { AppComponent } from &quot;./app/app.component&quot;;

bootstrapApplication(AppComponent, appConfig).catch((err) =&gt;
  console.error(err)
);
</code></pre>
<p>The <code>app.config.ts</code> file replaces <code>AppModule</code>:</p>
<pre><code class="language-ts">// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from &quot;@angular/core&quot;;
import { provideRouter } from &quot;@angular/router&quot;;
import { routes } from &quot;./app.routes&quot;;

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
  ],
};
</code></pre>
<p>Routing is defined in a simple <code>Routes</code> array:</p>
<pre><code class="language-ts">// app.routes.ts
import { Routes } from &quot;@angular/router&quot;;

export const routes: Routes = [];
</code></pre>
<hr />
<h2>ABP Studio Support for Standalone Structure</h2>
<p>Starting with the latest release (insert version number here), ABP Studio fully supports Angular's standalone structure. While the new format is encouraged, module-based structure will continue to be supported for backwards compatibility.</p>
<p>To try it out, simply update your ABP Studio to create apps with the latest version.</p>
<hr />
<h2>What’s New in ABP Studio Templates?</h2>
<p>When you generate an app using the latest ABP Studio, the project structure aligns with Angular's standalone architecture.</p>
<p>This migration is split into four parts:</p>
<ol>
<li><strong>Package updates</strong></li>
<li><strong>Schematics updates</strong></li>
<li><strong>Suite code generation updates</strong></li>
<li><strong>Template refactors</strong></li>
</ol>
<hr />
<h2>Package Migration Details</h2>
<p>Migration has been applied to packages in the <a href="https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages">ABP GitHub repository</a>. Here is an example from the Identity package.</p>
<h3>🧩 Migrating Components</h3>
<p>Components are made standalone, using:</p>
<pre><code class="language-bash">ng g @angular/core:standalone
</code></pre>
<p>Example:</p>
<pre><code class="language-ts">@Component({
  selector: 'abp-roles',
  templateUrl: './roles.component.html',
  providers: [...],
  imports: [
    ReactiveFormsModule,
    LocalizationPipe,
    ...
  ],
})
export class RolesComponent implements OnInit { ... }
</code></pre>
<h3>🛣 Updating Routing</h3>
<p>Old lazy-loaded routes using <code>forLazy()</code>:</p>
<pre><code class="language-ts">{
  path: 'identity',
  loadChildren: () =&gt; import('@abp/ng.identity').then(m =&gt; m.IdentityModule.forLazy({...}))
}
</code></pre>
<p>Now replaced with:</p>
<pre><code class="language-ts">{
  path: 'identity',
  loadChildren: () =&gt; import('@abp/ng.identity').then(c =&gt; c.createRoutes({...}))
}
</code></pre>
<h3>🧱 Replacing Module Declarations</h3>
<p>The old setup:</p>
<pre><code class="language-ts">// identity.module.ts
@NgModule({
  imports: [IdentityRoutingModule, RolesComponent, UsersComponent],
})
export class IdentityModule {...}
</code></pre>
<pre><code class="language-ts">//identity-routing.module
const routes: Routes = [...];
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class IdentityRoutingModule {}
</code></pre>
<p>New setup:</p>
<pre><code class="language-ts">// identity-routes.ts
export function provideIdentity(options: IdentityConfigOptions = {}): Provider[] {
  return [...];
}
export const createRoutes = (options: IdentityConfigOptions = {}): Routes =&gt; [
  {
    path: '',
    component: RouterOutletComponent,
    providers: provideIdentity(options),
    children: [
      {
        path: 'roles',
        component: ReplaceableRouteContainerComponent,
        data: {
          requiredPolicy: 'AbpIdentity.Roles',
          replaceableComponent: {
            key: eIdentityComponents.Roles,
            defaultComponent: RolesComponent,
          },
        },
        title: 'AbpIdentity::Roles',
      },
      ...
    ],
  },
];
</code></pre>
<hr />
<h2>ABP Schematics Migration Details</h2>
<p>You can reach details by checking <a href="https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/schematics">ABP Schematics codebase</a>.</p>
<h3>📚 Library creation</h3>
<p>When you run the <code>abp create-lib</code> command, the prompter will ask you the <code>templateType</code>. It supports both module and standalone templates.</p>
<pre><code class="language-ts">&quot;templateType&quot;: {
  &quot;type&quot;: &quot;string&quot;,
  &quot;description&quot;: &quot;Type of the template&quot;,
  &quot;enum&quot;: [&quot;module&quot;, &quot;standalone&quot;],
  &quot;x-prompt&quot;: {
    &quot;message&quot;: &quot;Select the type of template to generate:&quot;,
    &quot;type&quot;: &quot;list&quot;,
    &quot;items&quot;: [
      { &quot;value&quot;: &quot;module&quot;, &quot;label&quot;: &quot;Module Template&quot; },
      { &quot;value&quot;: &quot;standalone&quot;, &quot;label&quot;: &quot;Standalone Template&quot; }
    ]
  }
},
</code></pre>
<hr />
<h2>ABP Suite Code Generation Migration Details</h2>
<p>ABP Suite will also be supporting both structures. If you have a project that is generated with the previous versions, the Suite will detect the structure in that way and generate the related code accordingly. Conversely, here is what is changed for the standalone migration:</p>
<p><strong>❌ Discarded module files</strong></p>
<pre><code class="language-ts">// entity-one.module.ts
@NgModule({
  declarations: [],
  imports: [EntityOneComponent, EntityOneRoutingModule],
})
export class EntityOneModule {}
</code></pre>
<pre><code class="language-ts">// entity-one-routing.module.ts
export const routes: Routes = [
  {
    path: &quot;&quot;,
    component: EntityOneComponent,
    canActivate: [authGuard, permissionGuard],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class EntityOneRoutingModule {}
</code></pre>
<pre><code class="language-ts">// app-routing.module.ts
{
  path: 'entity-ones',
  loadChildren: () =&gt;
    import('./entity-ones/entity-one/entity-one.module').then(m =&gt; m.EntityOneModule),
},
</code></pre>
<p><strong>✅ Added routes configuration</strong></p>
<pre><code class="language-ts">// entity-one.routes.ts
export const ENTITY_ONE_ROUTES: Routes = [
  {
    path: &quot;&quot;,
    loadComponent: () =&gt; {
      return import(&quot;./components/entity-one.component&quot;).then(
        (c) =&gt; c.EntityOneComponent
      );
    },
    canActivate: [authGuard, permissionGuard],
  },
];
</code></pre>
<pre><code class="language-ts">// app.routes.ts
{ path: 'entity-ones', children: ENTITY_ONE_ROUTES },
</code></pre>
<hr />
<h2>Template Migration Details</h2>
<h3>🧭 Routing: <code>app.routes.ts</code></h3>
<pre><code class="language-ts">// app.routes.ts
import { Routes } from '@angular/router';

export const APP_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadComponent: () =&gt; import('./home/home.component').then(m =&gt; m.HomeComponent),
  },
  {
    path: 'account',
    loadChildren: () =&gt; import('@abp/ng.account').then(m =&gt; m.createRoutes()),
  },
  ...
];
</code></pre>
<h3>⚙ Configuration: <code>app.config.ts</code></h3>
<pre><code class="language-ts">// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(APP_ROUTES),
    APP_ROUTE_PROVIDER,
    provideAbpCore(
      withOptions({
        environment,
        registerLocaleFn: registerLocale(),
        ...
      })
    ),
    provideAbpOAuth(),
    provideAbpThemeShared(),
    ...
  ],
};

</code></pre>
<h3>🧼 Removed: <code>shared.module.ts</code></h3>
<p>This file has been removed to reduce unnecessary shared imports. Components now explicitly import what they need—leading to better encapsulation and less coupling.</p>
<hr />
<h2>Common Problems</h2>
<p>You may encounter these common problems that you would need to manage.</p>
<h3>1. Missing Imports</h3>
<p>In standalone structure, components must declare all their dependencies in <code>imports</code>. Forgetting this often causes template errors.</p>
<h3>2. Mixed Structures</h3>
<p>Combining modules and standalone in the same feature leads to confusion. Migrate features fully or keep them module-based.</p>
<h3>3. Routing Errors</h3>
<p>Incorrect migration from <code>forLazy()</code> to <code>createRoutes()</code> or <code>loadComponent</code> can break navigation. Double-check route configs.</p>
<h3>4. Service Injection</h3>
<p>Services provided in old modules may be missing. Add them in the component’s <code>providers</code> or <code>app.config.ts</code>.</p>
<h3>5. Shared Module Habit</h3>
<p>Reintroducing a shared module reduces the benefits of standalone. Import dependencies directly where needed.</p>
<hr />
<h2>Conclusion</h2>
<p>Angular’s standalone component architecture is a significant improvement for scalability, simplicity, and performance. With latest version of ABP Studio, you can adopt this modern approach with ease—without losing support for existing module-based projects.</p>
<p><strong>Ready to modernize your Angular development?</strong></p>
<p>Update your ABP Studio today and start building with standalone power!</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c68a7-4a41-4c91-088f-02f404258025" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c68a7-4a41-4c91-088f-02f404258025" medium="image" />
    </item>
  </channel>
</rss>