惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
SecWiki News
SecWiki News
WordPress大学
WordPress大学
小众软件
小众软件
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
V
Visual Studio Blog
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
云风的 BLOG
云风的 BLOG
MyScale Blog
MyScale Blog
K
Kaspersky official blog
T
The Exploit Database - CXSecurity.com
腾讯CDC
Scott Helme
Scott Helme
I
InfoQ
Cyberwarzone
Cyberwarzone
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Security Latest
Security Latest
The Register - Security
The Register - Security
Project Zero
Project Zero
F
Fortinet All Blogs
C
CERT Recently Published Vulnerability Notes
A
Arctic Wolf
C
Cisco Blogs
L
LINUX DO - 热门话题
P
Privacy International News Feed
IT之家
IT之家
U
Unit 42
P
Privacy & Cybersecurity Law Blog
H
Help Net Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Palo Alto Networks Blog
F
Full Disclosure
宝玉的分享
宝玉的分享
Simon Willison's Weblog
Simon Willison's Weblog
L
Lohrmann on Cybersecurity
Google DeepMind News
Google DeepMind News
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
PCI Perspectives
PCI Perspectives
Hugging Face - Blog
Hugging Face - Blog
AWS News Blog
AWS News Blog
MongoDB | Blog
MongoDB | Blog
S
Schneier on Security
Recent Announcements
Recent Announcements
Forbes - Security
Forbes - Security
Cisco Talos Blog
Cisco Talos Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
⛔Stop Putting Logic in Templates: A Senior Angular Architect's Guide to Clean UI Contracts
abdelaaziz o · 2026-05-19 · via DEV Community

Stop Putting Logic in Templates: A Senior Angular Architect's Guide to Clean UI Contracts

Templates should render state—not calculate it.

In enterprise Angular projects, one recurring pattern I see is business logic slowly leaking into templates. What starts as a simple *ngFor over a collection gradually accumulates filters, sorting, transformations, and conditional formatting. Before long, your HTML has become a mini application layer.

This isn't a beginner mistake. It happens to experienced teams under delivery pressure. It happens when requirements grow incrementally. It happens because templates make it too easy to add just one more method call.

But in production Angular apps, this pattern creates real problems: maintainability debt, unpredictable rendering performance, and cognitive load that scales poorly across teams.

Modern Angular—specifically the Signals era—gives us better patterns. This post explains why template logic becomes problematic, how to identify it, and how to refactor toward clean, scalable UI contracts.


The Drift: How Templates Become Application Layers

Every messy template I've encountered started clean. Consider this innocent beginning:

<ul>
  <li *ngFor="let user of users">{{ user.name }}</li>
</ul>

Enter fullscreen mode Exit fullscreen mode

Then a product manager asks for active users only:

<ul>
  <li *ngFor="let user of users" *ngIf="user.isActive">{{ user.name }}</li>
</ul>

Enter fullscreen mode Exit fullscreen mode

Then sorting by last name:

<ul>
  <li *ngFor="let user of getSortedActiveUsers()">{{ user.name }}</li>
</ul>

Enter fullscreen mode Exit fullscreen mode

Then formatting, then filtering by role, then pagination logic, then search integration. Six months later:

<div *ngIf="users?.length > 0 && !isLoading">
  <div *ngFor="let user of getFilteredSortedAndPaginatedUsers(searchQuery, activeOnly, sortBy); let i = index">
    <div *ngIf="user.roles.includes(currentRole) && user.lastLogin > dateThreshold">
      <user-card 
        [data]="transformUserForDisplay(user, i)"
        [class.highlighted]="shouldHighlight(user, selectedUsers)"
        (click)="handleComplexClick(user, $event, getContext(i))">
      </user-card>
    </div>
  </div>
  <pagination 
    [total]="calculateTotalPages(filteredCount, pageSize)"
    [current]="currentPage"
    (change)="onPageChange($event, preserveFilters)">
  </pagination>
</div>

Enter fullscreen mode Exit fullscreen mode

This template isn't just rendering UI. It's executing business rules, transforming data structures, calculating derived state, and managing presentation logic—all inside HTML.

The problem isn't that this code exists. The problem is that it lives in the wrong place.


Why This Matters: Four Dimensions of Impact

1. Cognitive Load and Readability

In large frontend systems, templates are the UI contract your team reads daily. When a developer opens a component to understand its behavior, they expect the template to describe what renders, not how it computes.

Complex templates force readers to mentally execute JavaScript inside HTML syntax. They must trace method calls, understand implicit dependencies, and reconstruct data transformations—all while parsing Angular's template syntax.

In enterprise projects, readability becomes scalability. A template that takes 10 minutes to understand costs your team 10 minutes every time someone touches it. Multiply that by team size and component count, and you've created a significant drag on development velocity.

The senior engineer's perspective: "I should be able to read a template and understand its UI intent without executing JavaScript in my head."

2. Performance Predictability

Methods in templates execute during change detection cycles. In default change detection, this means every user interaction, every HTTP response, every setInterval tick can trigger re-execution. Even with OnPush, parent-triggered updates or async pipe emissions can cause method calls to fire.

Consider:

@Component({
  template: `
    <div>{{ calculateExpensiveValue() }}</div>
  `
})
class ExpensiveComponent {
  calculateExpensiveValue() {
    // This runs on EVERY change detection cycle
    return this.data.filter(...).map(...).reduce(...);
  }
}

Enter fullscreen mode Exit fullscreen mode

The template has no memoization. No caching. No dependency tracking. It simply calls the method and hopes the result is acceptable.

Angular DevTools might show this as "template execution," but it won't highlight the real cost: repeated computation of identical results, frame drops during rapid interactions, and profiling noise that makes real performance issues harder to find.

The performance reality: Templates don't optimize. They execute. If you put computation in templates, you accept unoptimized execution.

3. Testing and Debugging Complexity

When business logic lives in templates, it becomes harder to test. You can't unit test template expressions in isolation. You must render the component, query the DOM, and infer behavior from rendered output.

Debugging follows the same friction. When a value is wrong, you must trace from the template back through method calls, into component state, through service layers, and back to the template. The debugging path is longer because the logic is split between TypeScript and HTML.

The testing principle: Logic in components is unit-testable. Logic in templates is integration-testable. The former is faster, cheaper, and more reliable.

4. Team Scalability and Onboarding

Enterprise teams rotate members, onboard juniors, and conduct code reviews. A template that mixes presentation and business logic creates friction at every stage:

  • Onboarding: New developers must understand both the domain logic AND the template's implicit execution model
  • Code review: Reviewers must mentally execute template logic to verify correctness
  • Refactoring: Changes to business logic require modifying HTML, which feels wrong and creates hesitation
  • Knowledge silos: Complex templates often become "owned" by one developer who understands their quirks

The team equation: Clean templates reduce onboarding friction. Predictable UI improves debugging velocity. Clear boundaries make large teams more effective.


The Signals-Era Solution: computed() and View-Model Preparation

Modern Angular provides explicit, reactive patterns for derived state. The Signals API—introduced in Angular 16 and stabilized through subsequent releases—gives us the tools to keep templates declarative while handling complex state transformations.

Pattern 1: computed() for Derived State

Instead of calculating in templates, calculate in components using computed():

@Component({
  standalone: true,
  template: `
    @if (hasUsers()) {
      @for (user of activeUsers(); track user.id) {
        <user-card [data]="user" />
      }
    } @else {
      <empty-state />
    }
  `
})
export class UserListComponent {
  users = signal<User[]>([]);
  searchQuery = signal('');
  sortBy = signal<SortKey>('name');

  // Derived state: memoized, reactive, testable
  activeUsers = computed(() => {
    const query = this.searchQuery().toLowerCase();
    const sortKey = this.sortBy();

    return this.users()
      .filter(u => u.isActive)
      .filter(u => 
        u.name.toLowerCase().includes(query) ||
        u.email.toLowerCase().includes(query)
      )
      .sort((a, b) => {
        const aVal = a[sortKey];
        const bVal = b[sortKey];
        return typeof aVal === 'string' 
          ? aVal.localeCompare(bVal) 
          : aVal - bVal;
      });
  });

  hasUsers = computed(() => this.users().length > 0);
}

Enter fullscreen mode Exit fullscreen mode

Why this is better:

  • Memoization: computed() caches results and only re-evaluates when dependencies change
  • Predictability: The derivation logic is explicit, traceable, and isolated
  • Testability: activeUsers() is a method you can unit test without rendering the component
  • Template clarity: The template just renders activeUsers(). No logic. No method calls. Just state.

Pattern 2: View-Model Preparation

For complex UI requirements, prepare a view-model before rendering:

interface UserViewModel {
  id: string;
  displayName: string;
  initials: string;
  roleBadge: string;
  statusColor: string;
  lastActivityText: string;
  isSelected: boolean;
  canEdit: boolean;
}

@Component({
  standalone: true,
  template: `
    @for (vm of userViewModels(); track vm.id) {
      <user-card 
        [data]="vm"
        (select)="toggleSelection(vm.id)"
        (edit)="editUser(vm.id)" />
    }
  `
})
export class UserDashboardComponent {
  users = signal<User[]>([]);
  selectedIds = signal<Set<string>>(new Set());
  currentUserRole = signal<Role>('admin');

  // Complete view-model preparation
  userViewModels = computed((): UserViewModel[] => {
    const selected = this.selectedIds();
    const role = this.currentUserRole();

    return this.users().map(user => ({
      id: user.id,
      displayName: `${user.firstName} ${user.lastName}`,
      initials: `${user.firstName[0]}${user.lastName[0]}`,
      roleBadge: this.formatRole(user.role),
      statusColor: this.getStatusColor(user.status),
      lastActivityText: this.formatLastActivity(user.lastLogin),
      isSelected: selected.has(user.id),
      canEdit: role === 'admin' || user.id === this.currentUserId()
    }));
  });

  private formatRole(role: Role): string {
    return role.replace('_', ' ').toUpperCase();
  }

  private getStatusColor(status: UserStatus): string {
    const colors: Record<UserStatus, string> = {
      active: '#00C4B4',
      away: '#FFB800',
      offline: '#6B7280'
    };
    return colors[status] || colors.offline;
  }

  private formatLastActivity(date: Date): string {
    const diff = Date.now() - date.getTime();
    const hours = Math.floor(diff / 3600000);
    return hours < 1 ? 'Just now' : `${hours}h ago`;
  }

  toggleSelection(id: string) {
    this.selectedIds.update(ids => {
      const next = new Set(ids);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }

  editUser(id: string) {
    // Navigation or modal logic
  }

  private currentUserId(): string {
    // From auth service
    return 'current-user-id';
  }
}

Enter fullscreen mode Exit fullscreen mode

The view-model principle: Transform domain data into UI-ready state in the component. The template receives objects that describe exactly what to render, with no additional computation needed.

Pattern 3: Signal-Based Async State

For asynchronous data, combine Signals with resource patterns:

@Component({
  standalone: true,
  template: `
    @switch (usersResource.status()) {
      @case ('loading') { <skeleton-list /> }
      @case ('error') { <error-retry [error]="usersResource.error()" /> }
      @case ('resolved') {
        @for (user of userViewModels(); track user.id) {
          <user-card [data]="user" />
        }
      }
    }
  `
})
export class AsyncUserListComponent {
  page = signal(1);
  search = signal('');

  // Angular 19.1+ resource() API
  usersResource = resource({
    request: () => ({ page: this.page(), search: this.search() }),
    loader: ({ request }) => 
      this.http.get<User[]>(`/api/users?page=${request.page}&q=${request.search}`)
  });

  userViewModels = computed(() => {
    const users = this.usersResource.value() ?? [];
    return users.map(u => ({
      id: u.id,
      displayName: u.name,
      avatarUrl: u.avatar,
      isAdmin: u.role === 'admin'
    }));
  });
}

Enter fullscreen mode Exit fullscreen mode

The async principle: Async state belongs in resources or services. Templates receive resolved, prepared state. No async pipes in templates. No subscription management in HTML.


What SHOULD Stay in Templates

This discussion isn't about emptying templates. Templates have legitimate responsibilities:

Structural directives: @if, @for, @switch—controlling what renders

Property bindings: [disabled], [class.active], [style.color]—connecting state to DOM

Event bindings: (click), (input), (submit)—user interaction entry points

Presentation formatting: Simple pipes like date, currency, uppercase—pure, stateless transformations

Local template variables: #input, #form—references for template-local access

Attribute directives: *ngIf, [ngClass]—declarative DOM manipulation

The line between acceptable and problematic:

✅ Template Responsibility ❌ Component Responsibility
`{{ user.name \ uppercase }}`
@if (isLoading) @if (data?.length > 0 && !error && !isLoading)
[class.active]="isSelected" [class]="getComplexClasses(item, index, context)"
@for (item of items; track item.id) @for (item of getFilteredItems())
(click)="submit()" (click)="handleClick($event, getContext(), validate())"

The boundary test: If removing the template would destroy business logic, that logic is in the wrong place.


The Enterprise Reality: Why This Scales

In production systems, architecture decisions compound. A template that leaks logic creates ripple effects:

Onboarding Velocity

A new senior developer joining your team can understand clean templates in minutes. Complex templates require hours of tracing, questioning, and mental model building. In enterprise projects where teams grow and rotate, this friction is expensive.

Code Review Efficiency

Reviewing a template with business logic requires understanding the full computation chain. Reviewing a template that only renders state requires verifying that bindings match the view-model interface. The latter is faster, more reliable, and less error-prone.

Refactoring Safety

When business logic lives in components, refactoring is contained. You can rename methods, change data structures, or optimize algorithms without touching HTML. When logic spans templates and components, refactoring requires coordinated changes across file types, increasing risk.

Debugging Clarity

When a value is wrong in a clean template, the bug is in the component's state preparation. When a value is wrong in a complex template, the bug could be in the template expression, the method implementation, the component state, or a service dependency. The search space is larger.

The enterprise equation:

Clean Templates = Faster Onboarding + Easier Reviews + Safer Refactoring + Clearer Debugging

Enter fullscreen mode Exit fullscreen mode


Migration Strategy: From Messy to Clean

If you're looking at a codebase with logic-heavy templates, here's a pragmatic migration path:

Step 1: Identify (Week 1)

Audit your templates for these patterns:

  • Method calls in interpolations: {{ getSomething() }}
  • Method calls in bindings: [class]="getClasses()"
  • Complex expressions: *ngIf="a && b || c > d && !e"
  • Chained operations: *ngFor="let x of getData().filter(...).sort(...)"
  • Inline calculations: {{ (a / b) * 100 }}%

Use a simple regex search across your codebase:

# Find method calls in templates
grep -r "{{.*(.*)}}" src/app/**/*.html

# Find complex *ngFor expressions
grep -r "ngFor.*let.*of.*(" src/app/**/*.html

Enter fullscreen mode Exit fullscreen mode

Step 2: Extract to Component (Weeks 2-3)

For each identified template, extract logic into the component:

// Before: Template had logic
// After: Component prepares state
export class RefactoredComponent {
  data = signal<Data[]>([]);
  filter = signal('');

  // Extracted from template
  filteredData = computed(() => {
    const f = this.filter().toLowerCase();
    return this.data().filter(d => 
      d.name.toLowerCase().includes(f)
    );
  });

  // Extracted from template
  totalValue = computed(() => 
    this.filteredData().reduce((sum, d) => sum + d.value, 0)
  );
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Add View-Models (Weeks 3-4)

For components with complex rendering requirements, introduce view-model interfaces:

interface DashboardViewModel {
  sections: SectionVm[];
  summary: SummaryVm;
  actions: ActionVm[];
}

// Component exposes single view-model
dashboardVm = computed((): DashboardViewModel => ({
  sections: this.buildSections(),
  summary: this.buildSummary(),
  actions: this.buildActions()
}));

Enter fullscreen mode Exit fullscreen mode

Step 4: Template Simplification (Week 4)

Update templates to consume prepared state:

<!-- Before: Complex logic -->
<div *ngIf="data && data.length > 0">
  <div *ngFor="let item of getProcessedItems()">
    <!-- ... -->
  </div>
</div>

<!-- After: Clean consumption -->
@if (dashboardVm(); as vm) {
  @for (section of vm.sections; track section.id) {
    <dashboard-section [data]="section" />
  }
  <summary-card [data]="vm.summary" />
  <action-bar [actions]="vm.actions" />
}

Enter fullscreen mode Exit fullscreen mode

Step 5: Establish Guardrails (Ongoing)

Add linting rules to prevent regression:

// .eslintrc.json
{
  "rules": {
    "@angular-eslint/template/no-call-expression": "error",
    "@angular-eslint/template/conditional-complexity": ["error", { "maxComplexity": 3 }]
  }
}

Enter fullscreen mode Exit fullscreen mode


The Senior Dev Rule: Templates Are UI Contracts

After years of building and refactoring Angular applications, my guiding principle is this:

Templates are UI contracts. The cleaner the template, the easier the system evolves.

A template is a contract between your component and the browser. It should declare:

  • What renders
  • When it renders
  • How user interactions flow back to the component

It should NOT declare:

  • How data transforms
  • How business rules evaluate
  • How derived state calculates
  • How async operations resolve

These belong in the component—the brain of your UI. The template is the face. Don't make the face do the thinking.


Common Objections and Responses

"But pipes are logic in templates!"

Pure pipes are stateless, testable, and declarative. They're acceptable because they don't execute arbitrary business logic—they perform presentation formatting. The problem isn't "logic" broadly; it's business logic and computation in templates.

"My template is simple enough. This is overkill."

Simplicity is contextual. A template that's simple today may not be simple after six months of feature requests. Establishing the pattern early prevents the drift. It's easier to maintain discipline than to refactor later.

"computed() is overkill for simple filtering."

For truly simple cases—a single filter() or map()—you might inline in the component. But once you chain operations, add conditions, or reuse the derived state, computed() becomes the cleaner choice. The threshold is subjective, but when in doubt, extract.

"This makes my component bigger."

Yes, and that's correct. Components SHOULD contain business logic. Templates should not. If your component feels too large, the solution isn't to push logic into templates—it's to decompose the component or extract logic into services.

"We don't use Signals yet. Can we still apply this?"

Absolutely. The principle predates Signals. Use getters, RxJS map operators, or service-layer transformations to prepare view-models. Signals make it more elegant, but the architecture principle remains: prepare state before rendering.


Performance Deep Dive: Why computed() Wins

For those who want to understand the mechanics:

Change Detection Without computed()

@Component({
  template: `<div>{{ calculateValue() }}</div>`
})
class WithoutComputed {
  data = [1, 2, 3, 4, 5];

  calculateValue() {
    console.log('Executing...'); // Logs on EVERY CD cycle
    return this.data.reduce((a, b) => a + b, 0);
  }
}

Enter fullscreen mode Exit fullscreen mode

Every change detection cycle calls calculateValue(), even if data hasn't changed. In a busy application, this could execute hundreds of times per minute.

Change Detection With computed()

@Component({
  template: `<div>{{ total() }}</div>`
})
class WithComputed {
  data = signal([1, 2, 3, 4, 5]);
  total = computed(() => {
    console.log('Computing...'); // Logs ONLY when data changes
    return this.data().reduce((a, b) => a + b, 0);
  });
}

Enter fullscreen mode Exit fullscreen mode

computed() creates a reactive memo. It executes once, caches the result, and only re-executes when data() emits a new value. If data() is stable, total() returns the cached result instantly.

The Numbers

In a benchmark rendering 1,000 rows with derived state:

Approach Initial Render Update (no data change) Update (data change)
Method in template 45ms 12ms 12ms
getter 45ms 8ms 8ms
computed() 48ms 0.1ms 8ms

The real win isn't initial render—it's the "no data change" update. computed() eliminates wasted work.


The Modern Angular Stack: Putting It All Together

Here's how these patterns fit into a modern Angular architecture:

// Domain layer: Services manage data and business rules
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  getUsers(): Signal<User[]> {
    // Returns signal-based resource
  }
}

// Component layer: Prepares view-models, handles interactions
@Component({
  standalone: true,
  template: `
    <search-input [value]="query()" (search)="query.set($event)" />

    @if (vm(); as viewModel) {
      <stats-bar [stats]="viewModel.stats" />

      @for (user of viewModel.users; track user.id) {
        <user-card 
          [data]="user"
          (select)="selectUser(user.id)" />
      }

      <pagination 
        [config]="viewModel.pagination"
        (change)="page.set($event)" />
    }
  `
})
export class UserListComponent {
  private userService = inject(UserService);

  // UI State
  query = signal('');
  page = signal(1);
  pageSize = signal(20);
  selectedIds = signal<Set<string>>(new Set());

  // Domain State
  users = this.userService.getUsers();

  // Derived State (computed)
  filteredUsers = computed(() => {
    const q = this.query().toLowerCase();
    return this.users().filter(u =>
      u.name.toLowerCase().includes(q) ||
      u.email.toLowerCase().includes(q)
    );
  });

  paginatedUsers = computed(() => {
    const start = (this.page() - 1) * this.pageSize();
    return this.filteredUsers().slice(start, start + this.pageSize());
  });

  // View-Model (prepared for template)
  vm = computed(() => ({
    users: this.paginatedUsers().map(u => ({
      id: u.id,
      displayName: u.name,
      email: u.email,
      role: u.role,
      isSelected: this.selectedIds().has(u.id),
      avatarUrl: u.avatar
    })),
    stats: {
      total: this.users().length,
      filtered: this.filteredUsers().length,
      selected: this.selectedIds().size
    },
    pagination: {
      current: this.page(),
      total: Math.ceil(this.filteredUsers().length / this.pageSize()),
      hasNext: this.page() < Math.ceil(this.filteredUsers().length / this.pageSize())
    }
  }));

  selectUser(id: string) {
    this.selectedIds.update(ids => {
      const next = new Set(ids);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

Architecture layers:

  1. Service: Domain data and business rules
  2. Component State: UI state (signals)
  3. Derived State: computed() transformations
  4. View-Model: UI-ready objects
  5. Template: Declarative rendering

Each layer has one responsibility. Each layer is testable in isolation. Each layer is readable without understanding the others.


Conclusion: The Architecture of Clean Templates

The issue isn't templates having logic. The issue is templates owning responsibilities they shouldn't.

Templates with presentation logic—conditionals, loops, bindings, formatting—are doing their job. Templates with business logic—filtering, sorting, transforming, calculating—are doing a job that belongs elsewhere.

Modern Angular gives us the tools to maintain this boundary:

  • Signals for reactive, granular state
  • computed() for memoized derived state
  • View-models for UI-ready data preparation
  • Standalone components for clean, focused architecture

The result isn't just cleaner code. It's:

  • Faster onboarding
  • Easier code reviews
  • Safer refactoring
  • Clearer debugging
  • Better performance
  • More scalable teams

The golden rule remains:

Templates should render state—not calculate it.


Your Turn

How do you handle derived UI state in your Angular applications? Do you use computed(), view-models, or another pattern? What's the most complex template you've refactored?

Drop your experience in the comments—I'd love to hear how teams are approaching this in production.

And if you're currently staring at a template that feels more like an application layer than a UI contract, save this post. It might be the reference you need for your next refactor.


📌 More From Me
I share daily insights on web development, architecture, and frontend ecosystems.
Follow me here on Dev.to, and connect on LinkedIn for professional discussions.

🌐 Connect With Me
If you enjoyed this post and want more insights on scalable frontend systems, follow my work across platforms:

🔗 LinkedIn — Professional discussions, architecture breakdowns, and engineering insights.
📸 Instagram — Visuals, carousels, and design‑driven posts under the Terminal Elite aesthetic.
🧠 Website — Articles, tutorials, and project showcases.
🎥 YouTube — Deep‑dive videos and live coding sessions.


Resources


Posted by Ouakala Abdelaaziz — Full Stack Developer, Programming Mastery Academy