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

推荐订阅源

WordPress大学
WordPress大学
O
OpenAI News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
Webroot Blog
Webroot Blog
GbyAI
GbyAI
S
SegmentFault 最新的问题
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
J
Java Code Geeks
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 【当耐特】
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
S
Security Affairs
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
Spread Privacy
Spread Privacy
T
Threatpost
Forbes - Security
Forbes - Security
C
Cisco Blogs
Scott Helme
Scott Helme
Attack and Defense Labs
Attack and Defense Labs
Simon Willison's Weblog
Simon Willison's Weblog
腾讯CDC
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
Last Week in AI
Last Week in AI
Recorded Future
Recorded Future
小众软件
小众软件
V
Vulnerabilities – Threatpost
美团技术团队
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
Martin Fowler
Martin Fowler
博客园 - 聂微东

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
💡Smart vs Presentational Components in Angular 2026: Where Does Ownership Really Belong?
abdelaaziz o · 2026-05-18 · via DEV Community

"Smart vs Presentational components didn't die. Angular just changed where the 'smartness' belongs."

Large Angular codebases in 2026 reveal a clear truth: the pattern that kills scalability fastest isn’t “too many components” —it’s
unclear ownership.

Here’s what’s actually changed this year 👇.

Every team I've worked with that struggled to maintain their Angular app had the same root problem: nobody could answer "who owns this state?" cleanly. The components were there. The services were there. But the boundaries were invisible, undocumented, and violated constantly.

This is the definitive 2026 breakdown of Smart vs Presentational component architecture — updated for Signals, standalone APIs, and the enterprise Angular teams shipping production code today.


Table of Contents

  1. Why This Pattern Still Matters in 2026
  2. The Old World: What We Got Wrong
  3. What Signals Actually Changed
  4. Presentational Components: The New Definition
  5. Smart Components: The Orchestration Layer
  6. The Real Question: Who Owns the State?
  7. Component Boundaries as Contracts
  8. The Signal() Decision Framework
  9. Code: The 2026 Pattern in Practice
  10. Anti-Patterns Still Destroying Enterprise Apps
  11. Enterprise Scalability Reasoning
  12. Feature Architecture and the Module Boundary Question
  13. Performance Implications
  14. Testing Strategy for Both Patterns
  15. Team Scalability and Cognitive Load
  16. The Contrarian Take
  17. Summary: The 2026 Mental Model

Why This Pattern Still Matters in 2026 {#why-this-pattern-still-matters}

In 2019, the Smart vs Presentational (also called Container vs Presentational, or Smart vs Dumb) debate felt settled. We had NgRx, we had async pipes, and we understood that containers fetch data while dumb components just render it.

Then Angular evolved fast:

  • Angular 14 introduced standalone components — no more NgModule for every component
  • Angular 16 shipped Signals as developer preview
  • Angular 17 stabilized signal(), computed(), effect(), signal-based input() and output()
  • Angular 18–19 matured the full reactive primitive system, with linkedSignal(), resource(), and server-side rendering improvements

Every one of these changes touched component responsibilities. And yet the fundamental question — who renders, who orchestrates, who owns the state — didn't disappear. It just became more important to answer correctly.

The teams that ignored this ended up with something worse than the "container monster" of the NgRx era: components that use Signals and inject services and handle routing and render UI — all while calling themselves "modern Angular."

Modern syntax does not equal modern architecture. That's the trap.


The Old World: What We Got Wrong {#the-old-world}

Let me describe a codebase I reviewed in 2022. It was a mid-size enterprise Angular app, well-intentioned, with 40+ developers contributing over 3 years.

The architecture looked correct on paper:

/features
  /user-management
    user-management.container.ts   ← "smart"
    user-list.component.ts         ← "presentational"
    user-card.component.ts         ← "presentational"
    user-filter.component.ts       ← "presentational"

Enter fullscreen mode Exit fullscreen mode

The reality inside those files was different:

user-management.container.ts — 1,100 lines. It fetched users, managed filters, handled pagination, subscribed to auth state, responded to router events, and passed 20+ inputs down to children. It had 14 injected dependencies. No one on the team could touch it without breaking something.

user-card.component.ts — Labeled "presentational." Injected AuthService to conditionally show a "delete" button. Had a local BehaviorSubject for hover state. Emitted a custom event but also directly called this.router.navigate() under certain conditions.

The labels were there. The separation wasn't.

What went wrong? The old mental model conflated size with responsibility. The container was "smart" because it was big. The card was "presentational" because it was small. Nobody enforced actual boundaries, and violation was invisible — just a service injection here, a router call there, until the architecture collapsed under its own weight.


What Signals Actually Changed {#what-signals-changed}

Signals didn't eliminate the Smart vs Presentational pattern. They clarified it.

Here's what changed:

Before Signals: State Forced Centralization

Without Signals, local reactive state was painful. You had three choices for component-level reactivity:

  1. BehaviorSubject — verbose, manual subscription management, easy to leak
  2. async pipe — clean, but only works in templates, no imperative access
  3. Pass everything from a parent container — which inflated containers

This meant components that should be simple ended up either leaking state or depending on a parent container to manage trivial UI state (an open/closed accordion, a hover state, a selected tab). The container grew. The "dumb" component stayed small but had invisible wiring dependencies.

After Signals: Local State Became Cheap

With signal(), a component can own its own ephemeral reactive state without any of that overhead:

// Before Signals — accordion toggle required BehaviorSubject or parent coordination
private _isOpen$ = new BehaviorSubject<boolean>(false);
isOpen$ = this._isOpen$.asObservable();
toggle() { this._isOpen$.next(!this._isOpen$.value); }

// After Signals — trivial, readable, zero subscription management
isOpen = signal(false);
toggle() { this.isOpen.update(v => !v); }

Enter fullscreen mode Exit fullscreen mode

This removed a whole class of "smart container needed here just to manage local state." Which means smart components can now focus on what they were always supposed to do: orchestrate business flows, not babysit UI toggle state.

What computed() Changed

Before: derived state meant a chain of combineLatest() operators, manual typing, and cognitive overhead.

After: derived state is a single, readable, synchronously-evaluated expression that Angular tracks automatically:

// Before
filteredUsers$ = combineLatest([this.users$, this.filter$]).pipe(
  map(([users, filter]) => users.filter(u => u.name.includes(filter)))
);

// After
filteredUsers = computed(() =>
  this.users()?.filter(u => u.name.includes(this.filter())) ?? []
);

Enter fullscreen mode Exit fullscreen mode

The logic is identical. The ceremony is gone. And critically: Angular's fine-grained reactivity means only components that read filteredUsers re-render when it changes — not the whole tree.

What Signal-Based Inputs Changed

Angular 17+ introduced input() as a signal-based replacement for @Input(). This is subtle but architecturally significant:

// Old decorator-based input
@Input() user!: User;

// New signal-based input — readable as a signal in the template and class
user = input.required<User>();

// In the template: {{ user().name }}
// In the class: const name = this.user().name; // synchronous, no subscription

Enter fullscreen mode Exit fullscreen mode

Presentational components can now derive state from their inputs reactively and locally, without any parent coordination:

@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class UserCardComponent {
  user = input.required<User>();
  isAdmin = input<boolean>(false);

  // Derived from inputs — no service, no parent, no subscription
  displayName = computed(() =>
    this.isAdmin() ? `[Admin] ${this.user().name}` : this.user().name
  );
}

Enter fullscreen mode Exit fullscreen mode

This is the Signals-era presentational component. It derives display logic from its inputs. It never reaches outside its own boundary.


Presentational Components: The New Definition {#presentational-components}

The 2026 definition of a presentational component isn't "small" or "dumb." It's about the contract it enforces.

The Contract

"Give me my data through inputs. I will render it. Tell me via outputs when something happens that you should know about."

That's it. That's the entire contract.

A presentational component:

  • Receives data through input() or @Input()
  • Emits events through output() or @Output()
  • May hold local UI-only state via signal() (open/closed, hover, selected tab within a group)
  • Derives display logic via computed() from its own inputs
  • Never injects a service
  • Never calls the router
  • Never triggers a side effect that escapes the component
  • Is fully testable with only input data — no service mocks needed

What "Presentational" Means for Your Design System

Presentational components are the atoms and molecules of your UI system. They include:

  • Buttons — variants, states, loading indicators
  • Form inputs — validation display, label, error state
  • Cards — layout, header/body/footer slots
  • Tables — column rendering, sort indicators, pagination display (not pagination logic)
  • Modals — open/close state, header/body/footer structure
  • Data visualizations — chart rendering from data passed as inputs
  • Navigation items — active state, icon, label
  • Status badges — color coding, label display

Notice: none of these need to know where the data comes from, how it was fetched, or what happens when the user's event is processed. That's not their job.

The Full 2026 Presentational Component Pattern

import {
  Component,
  ChangeDetectionStrategy,
  input,
  output,
  computed
} from '@angular/core';
import { CommonModule } from '@angular/common';

export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'member' | 'viewer';
  isActive: boolean;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="user-card" [class.inactive]="!user().isActive">
      <div class="user-card__header">
        <span class="user-card__name">{{ displayName() }}</span>
        <span class="user-card__badge" [attr.data-role]="user().role">
          {{ user().role }}
        </span>
      </div>
      <div class="user-card__body">
        <span class="user-card__email">{{ user().email }}</span>
      </div>
      <div class="user-card__actions">
        <button
          (click)="select.emit(user())"
          [disabled]="!user().isActive"
        >
          View Profile
        </button>
        @if (canDelete()) {
          <button class="danger" (click)="delete.emit(user().id)">
            Remove
          </button>
        }
      </div>
    </div>
  `
})
export class UserCardComponent {
  // ── Inputs ────────────────────────────────────────────
  user       = input.required<User>();
  showDelete = input<boolean>(false);

  // ── Outputs ───────────────────────────────────────────
  select = output<User>();
  delete = output<string>();

  // ── Local derived state (computed from inputs only) ───
  displayName = computed(() => {
    const u = this.user();
    return u.isActive ? u.name : `${u.name} (Inactive)`;
  });

  canDelete = computed(() => this.showDelete() && this.user().isActive);

  // ── Zero injected dependencies ────────────────────────
  // This component can be tested with zero mocks.
}

Enter fullscreen mode Exit fullscreen mode

Notice what's absent: no inject(), no constructor() with dependencies, no service calls, no router. The component is a pure function of its inputs. You can test it by instantiating it with different input values and asserting on the rendered output. That's the power of this contract.


Smart Components: The Orchestration Layer {#smart-components}

The modern name for "smart component" in enterprise Angular is the feature shell or feature orchestrator. The label matters less than the responsibility.

The Contract

"I own this feature flow. I decide what data gets loaded. I coordinate business events. I delegate all rendering to presentational children."

A smart component / feature shell:

  • Injects services for data access, routing, auth
  • Owns feature-level state via signal() and computed()
  • Converts observables to signals with toSignal()
  • Responds to child component events and processes them as business actions
  • Never renders data directly — delegates all rendering to presentational components
  • Is the entry point for a routed feature

Smart Component Size vs Responsibility

One misconception that persists: smart components should be small. Wrong. Smart components can be large — because orchestrating a feature is genuinely complex. The size isn't the problem. Mixed concerns are the problem.

A 300-line smart component that only orchestrates is fine. A 50-line "presentational" component that injects a service is not.

The Full 2026 Smart / Feature Shell Pattern

import {
  Component,
  ChangeDetectionStrategy,
  inject,
  signal,
  computed
} from '@angular/core';
import { Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';

import { UserService } from './data-access/user.service';
import { AuthStore } from '../auth/auth.store';
import { UserCardComponent } from './ui/user-card.component';
import { UserFiltersComponent } from './ui/user-filters.component';
import { UserTableComponent } from './ui/user-table.component';
import { User } from './models/user.model';

@Component({
  selector: 'app-user-management',
  standalone: true,
  imports: [
    UserCardComponent,
    UserFiltersComponent,
    UserTableComponent
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="feature-shell">
      <app-user-filters
        [activeFilter]="activeFilter()"
        (filterChange)="onFilterChange($event)"
      />

      @if (isLoading()) {
        <div class="loading-state">Loading users...</div>
      } @else if (error()) {
        <div class="error-state">{{ error() }}</div>
      } @else {
        <app-user-table
          [users]="filteredUsers()"
          [currentUserRole]="currentUserRole()"
          (userSelected)="onUserSelect($event)"
          (userDeleted)="onUserDelete($event)"
        />
      }
    </div>
  `
})
export class UserManagementComponent {
  // ── Dependencies (injected here — NOT in presentational children) ──
  private userSvc  = inject(UserService);
  private authStore = inject(AuthStore);
  private router   = inject(Router);

  // ── Feature state ─────────────────────────────────────────────────
  activeFilter  = signal<string>('');
  private _error = signal<string | null>(null);

  // ── Data from services, converted to signals ───────────────────────
  private users = toSignal(this.userSvc.getAll(), { initialValue: [] });
  isLoading     = toSignal(this.userSvc.loading$, { initialValue: false });
  error         = this._error.asReadonly();

  // ── Derived / computed state ──────────────────────────────────────
  filteredUsers = computed(() => {
    const term = this.activeFilter().toLowerCase();
    return term
      ? this.users().filter(u =>
          u.name.toLowerCase().includes(term) ||
          u.email.toLowerCase().includes(term)
        )
      : this.users();
  });

  currentUserRole = computed(() => this.authStore.user()?.role ?? 'viewer');

  // ── Business event handlers ───────────────────────────────────────
  onFilterChange(term: string): void {
    this.activeFilter.set(term);
  }

  onUserSelect(user: User): void {
    this.router.navigate(['/users', user.id]);
  }

  async onUserDelete(userId: string): Promise<void> {
    try {
      await this.userSvc.delete(userId);
    } catch (err) {
      this._error.set('Failed to delete user. Please try again.');
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Study what this component does and doesn't do:

Does: inject services, own state, compute derived data, respond to business events, navigate on selection, handle errors

Doesn't: render a single <tr>, <td>, user name, email, or any data-display HTML directly

That's the boundary. And it's enforced in the template itself — every rendered element is a component call, not raw HTML data display.


The Real Question: Who Owns the State? {#who-owns-the-state}

Here's the architectural question that replaces "is this smart or presentational?":

"Who should own this state — and why?"

There are three answers:

1. Local signal() — UI-only ephemeral state

Use local signal() when:

  • The state is purely about UI appearance (open/closed, hover, selected tab)
  • No other component needs to know about it
  • It doesn't affect business logic
  • It resets naturally when the component is destroyed
// ✅ Correct use of local signal — UI toggle state
export class AccordionSectionComponent {
  label    = input.required<string>();
  isOpen   = signal(false);
  toggle() { this.isOpen.update(v => !v); }
}

Enter fullscreen mode Exit fullscreen mode

2. Feature-level signal() in a smart component — shared feature state

Use smart-component-owned signals when:

  • Multiple presentational components in the same feature read the state
  • The state is derived from service data
  • The state has business significance (selected entity, active filter, loading status)
// ✅ Feature-level state — owned by the smart orchestration layer
export class ProductSearchComponent {
  private productSvc = inject(ProductService);
  searchTerm    = signal('');
  selectedId    = signal<string | null>(null);
  products      = toSignal(this.productSvc.getAll(), { initialValue: [] });
  filtered      = computed(() =>
    this.products().filter(p =>
      p.name.toLowerCase().includes(this.searchTerm().toLowerCase())
    )
  );
}

Enter fullscreen mode Exit fullscreen mode

3. A global store — cross-feature shared state

Use a store (NgRx Signals Store, Elf, or a custom Injectable signal store) when:

  • The state is shared across multiple features
  • The state needs to survive navigation (shopping cart, user preferences, notifications)
  • Multiple smart components need to sync on the same state
// ✅ Cross-feature state — injectable signal store
@Injectable({ providedIn: 'root' })
export class CartStore {
  private items = signal<CartItem[]>([]);

  readonly cartItems = this.items.asReadonly();
  readonly itemCount = computed(() => this.items().length);
  readonly total     = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
  );

  addItem(item: CartItem)    { this.items.update(items => [...items, item]); }
  removeItem(id: string)     { this.items.update(items => items.filter(i => i.id !== id)); }
  clear()                    { this.items.set([]); }
}

Enter fullscreen mode Exit fullscreen mode

The Ownership Decision Tree

Is this state about UI appearance only?
  ├── Yes → local signal() in the component
  └── No → Does it belong to one feature?
        ├── Yes → signal() in the feature's smart component
        └── No → Injectable signal store (or NgRx Signals store)

Enter fullscreen mode Exit fullscreen mode

When in doubt, push state up one level. It's far easier to move state down (by passing it as inputs) than to extract entangled state out of a deep component tree later.


Component Boundaries as Contracts {#component-boundaries-as-contracts}

This is the senior-developer insight that transforms how you think about component architecture:

A component boundary is a communication contract, not a folder name.

What does that mean in practice?

When you define a component's input() and output() signatures, you're defining an API. That API:

  • Can be enforced through TypeScript (input.required<T>() means no default — callers must provide it)
  • Can be tested independently with zero mocks (if presentational) or targeted mocks (if smart)
  • Can be documented through type signatures alone
  • Can be refactored without affecting other components — as long as the public API stays stable

The moment a "presentational" component injects a service, it has broken its contract. It now has an invisible dependency that doesn't appear in its inputs and outputs. Callers can't see it. Tests can't control it. Refactors break unpredictably.

Enforcing Boundaries with Architecture Lint Rules

In enterprise teams, verbal conventions don't scale. You need tools.

With eslint-plugin-angular, you can write custom rules. Or you can enforce patterns manually through review guidelines:

Convention: Presentational components live in /ui folders. Smart components live at the feature root.

/features
  /user-management
    user-management.component.ts          ← smart (feature shell)
    /ui
      user-card.component.ts              ← presentational
      user-table.component.ts             ← presentational
      user-filters.component.ts           ← presentational
    /data-access
      user.service.ts
    /models
      user.model.ts

Enter fullscreen mode Exit fullscreen mode

The convention itself communicates intent. A UserCardComponent inside /ui that injects a service is visibly wrong in a code review — because the folder declares its contract.


The signal() Decision Framework {#signal-decision-framework}

With Signals matured, here's a practical framework I use when reviewing Angular code:

Ask These Questions for Every signal() You See

1. Where is this signal declared?

  • In a presentational component (/ui folder) → it must be UI-only state. If it derives from service data, that's a violation.
  • In a smart component (feature shell) → it can own feature business state. Check if it belongs in a store instead.
  • In an @Injectable store → it's intentionally shared. Confirm the scope (providedIn: 'root' vs feature-provided).

2. Who reads this signal?

  • Only the declaring component's template → local signal, correct.
  • Another component in the same feature → feature-level state, should be in the smart component.
  • Components across features → global store, should be in an injectable service/store.

3. Does this signal have business significance?

  • If the value of this signal would matter to a backend API, a business rule, or a cross-component flow → it does not belong in a presentational component.
  • If it's purely about which CSS class to apply or whether a dropdown is open → local signal is correct.

4. What happens to this signal when the component is destroyed?

  • If the answer is "it resets and nobody cares" → local signal is fine.
  • If the answer is "the user loses their progress/selection" → the state belongs in a parent feature or store.

Code: The 2026 Pattern in Practice {#code-examples}

Let's walk through a complete, real-world-adjacent feature: a product search and selection flow.

Feature Structure

/products
  product-search.component.ts       ← smart (feature shell)
  /ui
    product-search-bar.component.ts  ← presentational
    product-list.component.ts        ← presentational
    product-card.component.ts        ← presentational
    product-detail-panel.component.ts ← presentational
  /data-access
    product.service.ts
  /models
    product.model.ts

Enter fullscreen mode Exit fullscreen mode

The Presentational Components

// product-search-bar.component.ts
@Component({
  selector: 'app-product-search-bar',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input
      type="search"
      [value]="value()"
      (input)="searchChange.emit($any($event.target).value)"
      placeholder="Search products..."
    />
  `
})
export class ProductSearchBarComponent {
  value        = input<string>('');
  searchChange = output<string>();
  // Zero injected services. Zero side effects. Pure I/O.
}

Enter fullscreen mode Exit fullscreen mode

// product-card.component.ts
@Component({
  selector: 'app-product-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="product-card" [class.selected]="isSelected()">
      <img [src]="product().imageUrl" [alt]="product().name" />
      <div class="product-card__info">
        <h3>{{ product().name }}</h3>
        <span class="price">{{ formattedPrice() }}</span>
        <span class="stock" [class.low]="isLowStock()">
          {{ stockLabel() }}
        </span>
      </div>
      <button (click)="select.emit(product())">
        {{ isSelected() ? 'Selected' : 'Select' }}
      </button>
    </div>
  `
})
export class ProductCardComponent {
  product    = input.required<Product>();
  isSelected = input<boolean>(false);
  select     = output<Product>();

  // All derived from inputs — no external dependencies
  formattedPrice = computed(() =>
    new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
      .format(this.product().price)
  );

  isLowStock  = computed(() => this.product().stock < 10);
  stockLabel  = computed(() =>
    this.isLowStock()
      ? `Only ${this.product().stock} left`
      : `In stock (${this.product().stock})`
  );
}

Enter fullscreen mode Exit fullscreen mode

The Smart Feature Shell

// product-search.component.ts
@Component({
  selector: 'app-product-search',
  standalone: true,
  imports: [
    ProductSearchBarComponent,
    ProductListComponent,
    ProductDetailPanelComponent
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="product-search-feature">
      <app-product-search-bar
        [value]="searchTerm()"
        (searchChange)="onSearch($event)"
      />

      <div class="product-search-feature__layout">
        <app-product-list
          [products]="filteredProducts()"
          [selectedId]="selectedProductId()"
          [isLoading]="isLoading()"
          (productSelected)="onProductSelect($event)"
        />

        @if (selectedProduct()) {
          <app-product-detail-panel
            [product]="selectedProduct()!"
            (addToCart)="onAddToCart($event)"
            (close)="onClose()"
          />
        }
      </div>
    </div>
  `
})
export class ProductSearchComponent {
  private productSvc = inject(ProductService);
  private cartStore  = inject(CartStore);
  private analytics  = inject(AnalyticsService);

  // ── State owned by this feature shell ─────────────────
  searchTerm        = signal('');
  selectedProductId = signal<string | null>(null);

  // ── Data from services ────────────────────────────────
  private allProducts = toSignal(this.productSvc.getAll(), { initialValue: [] });
  isLoading           = toSignal(this.productSvc.loading$, { initialValue: false });

  // ── Derived state ─────────────────────────────────────
  filteredProducts = computed(() => {
    const term = this.searchTerm().toLowerCase().trim();
    if (!term) return this.allProducts();
    return this.allProducts().filter(p =>
      p.name.toLowerCase().includes(term) ||
      p.description.toLowerCase().includes(term)
    );
  });

  selectedProduct = computed(() =>
    this.allProducts().find(p => p.id === this.selectedProductId()) ?? null
  );

  // ── Business event handlers ───────────────────────────
  onSearch(term: string): void {
    this.searchTerm.set(term);
    this.selectedProductId.set(null); // clear selection on new search
  }

  onProductSelect(product: Product): void {
    this.selectedProductId.set(product.id);
    this.analytics.track('product_viewed', { productId: product.id });
  }

  onAddToCart(product: Product): void {
    this.cartStore.addItem({ ...product, qty: 1 });
    this.analytics.track('product_added_to_cart', { productId: product.id });
    this.selectedProductId.set(null);
  }

  onClose(): void {
    this.selectedProductId.set(null);
  }
}

Enter fullscreen mode Exit fullscreen mode

Read this template carefully. Every element in it is a component call. No raw data rendering. The smart component's template is essentially a wiring diagram — it maps data flows and event responses between the feature state and presentational children.

That's intentional. The template of a smart component should read like an architecture diagram.


Anti-Patterns Still Destroying Enterprise Apps {#anti-patterns}

These are the violations I find most often in production Angular codebases in 2026:

Anti-Pattern 1: The Service-Injecting "Presentational" Component

// ❌ This is not a presentational component
@Component({ selector: 'app-user-card', standalone: true })
export class UserCardComponent {
  user    = input.required<User>();
  private authSvc = inject(AuthService);  // 🚨 Contract violation

  // Now this component's behavior depends on auth state
  // It can't be tested without mocking AuthService
  // It can't be reused in any context where AuthService isn't relevant
  canEdit = computed(() => this.authSvc.currentUser()?.role === 'admin');
}

Enter fullscreen mode Exit fullscreen mode

The fix: Pass canEdit as an input. Let the smart parent compute it. The card renders. The parent decides.

// ✅ Correct
export class UserCardComponent {
  user    = input.required<User>();
  canEdit = input<boolean>(false);  // Parent decides this, not the card
}

Enter fullscreen mode Exit fullscreen mode

Anti-Pattern 2: Routing from a Presentational Component

// ❌ Presentational component navigating
@Component({ selector: 'app-product-card', standalone: true })
export class ProductCardComponent {
  product = input.required<Product>();
  private router = inject(Router);  // 🚨 Side-effect leak

  viewDetails(): void {
    this.router.navigate(['/products', this.product().id]);
  }
}

Enter fullscreen mode Exit fullscreen mode

The fix: Emit the event. Let the smart parent decide what to do with it.

// ✅ Correct
export class ProductCardComponent {
  product     = input.required<Product>();
  viewDetails = output<Product>();  // Parent handles navigation
}

Enter fullscreen mode Exit fullscreen mode

Anti-Pattern 3: Business Logic in computed() Inside a Presentational Component

// ❌ Business rule in a presentational component
export class OrderCardComponent {
  order = input.required<Order>();

  // 🚨 "Discount eligibility" is a business rule, not a display rule
  isEligibleForDiscount = computed(() =>
    this.order().total > 100 &&
    this.order().status === 'pending' &&
    this.order().createdAt > someBusinessDate
  );
}

Enter fullscreen mode Exit fullscreen mode

The fix: Compute business rules in the smart parent or service. Pass the result as an input.

// ✅ Correct
export class OrderCardComponent {
  order              = input.required<Order>();
  showDiscountBadge  = input<boolean>(false);  // Smart parent computed this
}

Enter fullscreen mode Exit fullscreen mode

Anti-Pattern 4: The Feature-Spanning Presentational Component

This one is subtle. A "presentational" component that's used across multiple features but slowly accumulates feature-specific logic:

// ❌ "Shared" component that knows too much
@Component({ selector: 'app-data-table', standalone: true })
export class DataTableComponent {
  // Started as a pure table renderer...
  rows    = input.required<any[]>();
  columns = input.required<Column[]>();

  // ...then someone added feature-specific logic:
  private userSvc    = inject(UserService);    // 🚨
  private reportsSvc = inject(ReportsService); // 🚨
  private exportSvc  = inject(ExportService);  // 🚨
}

Enter fullscreen mode Exit fullscreen mode

The fix: Keep shared UI components pure. Provide feature-specific behavior through composition — wrap the shared component inside a feature-specific smart component that handles the services.

Anti-Pattern 5: Mixing signal() and BehaviorSubject Without Reason

In 2026, there's no architectural reason to use BehaviorSubject for new code in a standalone Angular app. The mixed reactive model creates cognitive overhead and inconsistency. Migrate to Signals systematically:

// ❌ Mixed reactive model — 2022 tech in 2026 code
export class FeatureComponent {
  private _filter$ = new BehaviorSubject<string>('');
  filter$          = this._filter$.asObservable();
  selectedId       = signal<string | null>(null);  // Some Signals

  // Now you need both pipe operators AND computed()
  // to derive combined state. Ugly.
}

// ✅ Consistent Signals approach
export class FeatureComponent {
  filter     = signal('');
  selectedId = signal<string | null>(null);

  // All derivations use computed() — consistent, readable
}

Enter fullscreen mode Exit fullscreen mode


Enterprise Scalability Reasoning {#enterprise-scalability}

Why does this pattern matter at scale? Here's the reasoning I give to engineering managers and CTOs.

Cognitive Load Reduction

When every component in a folder has a clear, enforced type — presentational OR smart — developers new to the codebase can understand what a component does before opening the file.

/ui/user-card.component.ts → rendering only, inputs/outputs, no surprises
user-management.component.ts → orchestration, services, state management

The folder structure communicates architecture. This reduces onboarding time and code review surface area.

Parallel Development

Presentational components that follow the contract can be built and reviewed in isolation, in parallel with feature development. Your design system team can build and ship UserCardComponent without waiting for the UserService to exist. The smart component composes them when both are ready.

This is how enterprise frontend teams ship faster — not by writing less code, but by working in parallel with clear, enforced boundaries.

Testability at Scale

Presentational component testing is trivially fast:

// ✅ Testing a presentational component — zero mocks needed
it('should display inactive label when user is inactive', () => {
  const fixture = TestBed.createComponent(UserCardComponent);
  fixture.componentInstance.user.set({ ...mockUser, isActive: false });
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('Inactive');
});

Enter fullscreen mode Exit fullscreen mode

Smart component testing focuses on business logic, using mocked services:

// ✅ Testing a smart component — mock the services, not the UI
it('should navigate to user profile on selection', () => {
  const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
  const component = new UserManagementComponent();
  // ... inject mocks
  component.onUserSelect(mockUser);
  expect(routerSpy.navigate).toHaveBeenCalledWith(['/users', mockUser.id]);
});

Enter fullscreen mode Exit fullscreen mode

At 100+ components, this separation means your test suite stays fast. Slow tests kill developer velocity. Slow developer velocity kills product delivery.

Refactoring Safety

When a presentational component has no injected dependencies, refactoring it is safe. You change the template, the inputs, the derived computeds — the blast radius is contained to its callers.

When a smart component changes its orchestration logic, the impact is contained to the feature. Presentational components used by that feature don't change.

At enterprise scale, containment is a feature.


Feature Architecture and the Module Boundary Question {#feature-architecture}

With standalone components eliminating the forced NgModule ceremony, the module question becomes architectural rather than syntactical.

Feature Boundaries with Standalone Components

/src/app
  /features
    /users                              ← Feature boundary
      index.ts                          ← Public API — what this feature exports
      user-management.routes.ts         ← Lazy-loaded routes
      user-management.component.ts      ← Feature shell (smart)
      /ui                               ← Private — only used within this feature
        user-card.component.ts
        user-table.component.ts
      /data-access                      ← Feature services
        user.service.ts
    /products                           ← Another feature boundary
      index.ts
      ...
  /shared                               ← Shared presentational components
    /ui
      button.component.ts
      data-table.component.ts
      modal.component.ts
    /utils
      ...

Enter fullscreen mode Exit fullscreen mode

The index.ts barrel exports define the feature's public API. Anything not exported from index.ts is considered internal to the feature and shouldn't be imported by other features.

This is a convention, not a hard compiler enforcement — but with ESLint rules from Sheriff or Nx module boundary rules, you can make it enforceable.

Injectable Stores as the Bridge

For state shared between features without NgRx's ceremony, the 2026 pattern is an @Injectable signal store:

// /shared/stores/notification.store.ts
@Injectable({ providedIn: 'root' })
export class NotificationStore {
  private _notifications = signal<Notification[]>([]);

  readonly all    = this._notifications.asReadonly();
  readonly unread = computed(() => this._notifications().filter(n => !n.isRead));
  readonly count  = computed(() => this.unread().length);

  push(notification: Notification): void {
    this._notifications.update(list => [notification, ...list]);
  }

  markRead(id: string): void {
    this._notifications.update(list =>
      list.map(n => n.id === id ? { ...n, isRead: true } : n)
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Any smart component anywhere can inject this store and read from it reactively. The signal-based reactivity means components only re-render when the specific signal they read changes — no unnecessary re-renders from unrelated state updates.


Performance Implications {#performance-implications}

The Smart vs Presentational architecture isn't just about code organization — it has direct performance implications in Signals-era Angular.

OnPush + Signals = Maximum Granularity

All presentational components should use ChangeDetectionStrategy.OnPush. With signal-based inputs, Angular can skip change detection for a component entirely if none of its signal dependencies changed.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush  // Always for presentational
})
export class UserCardComponent {
  user = input.required<User>();  // Signal-based — Angular tracks reads automatically
}

Enter fullscreen mode Exit fullscreen mode

When user signal doesn't change between renders, UserCardComponent is skipped entirely by the change detection pass. In a list of 100 user cards where only 3 changed, only those 3 re-render.

Signal Granularity vs. Object Reference

One performance trap with Signals: passing entire objects as signals when only one property changes.

// ❌ Less granular — entire user object is the signal value
// Changing any property triggers re-render of all components reading user()
user = signal<User>(initialUser);

// ✅ More granular — each property is independently reactive
// (when the component owns the data, not just passing it down)
userName   = signal('');
userEmail  = signal('');
userStatus = signal<'active' | 'inactive'>('active');

Enter fullscreen mode Exit fullscreen mode

For presentational components receiving data as inputs, this granularity decision belongs to the smart parent — another reason the smart component should own the state, so it can structure it optimally.

toSignal() and Initial Values

Always provide initialValue to toSignal() to avoid the undefined guard dance:

// ❌ Products might be undefined — template needs @if guard
products = toSignal(this.productSvc.getAll());

// ✅ Always has a value — template is simpler
products = toSignal(this.productSvc.getAll(), { initialValue: [] as Product[] });

Enter fullscreen mode Exit fullscreen mode


Testing Strategy for Both Patterns {#testing-strategy}

Presentational Component Tests: Fast and Isolated

The rule: if you need more than 2 TestBed.configureTestingModule imports to test a component, reconsider whether it's truly presentational.

describe('UserCardComponent', () => {
  beforeEach(() => TestBed.configureTestingModule({
    imports: [UserCardComponent]  // That's it. No service mocks.
  }));

  it('renders user name', () => {
    const fixture = TestBed.createComponent(UserCardComponent);
    fixture.componentRef.setInput('user', { name: 'Alice', email: 'a@test.com', isActive: true });
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('h3').textContent).toBe('Alice');
  });

  it('appends (Inactive) for inactive users', () => {
    const fixture = TestBed.createComponent(UserCardComponent);
    fixture.componentRef.setInput('user', { name: 'Bob', isActive: false });
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toContain('Bob (Inactive)');
  });

  it('emits select output on button click', () => {
    const fixture = TestBed.createComponent(UserCardComponent);
    const mockUser = { id: '1', name: 'Alice', isActive: true };
    fixture.componentRef.setInput('user', mockUser);

    const selectSpy = jasmine.createSpy();
    fixture.componentInstance.select.subscribe(selectSpy);

    fixture.detectChanges();
    fixture.nativeElement.querySelector('button').click();

    expect(selectSpy).toHaveBeenCalledWith(mockUser);
  });
});

Enter fullscreen mode Exit fullscreen mode

Smart Component Tests: Business Logic Focus

Smart component tests mock services and assert on state + navigation behavior:

describe('UserManagementComponent', () => {
  let userSvcMock: jasmine.SpyObj<UserService>;
  let routerMock: jasmine.SpyObj<Router>;

  beforeEach(() => {
    userSvcMock = jasmine.createSpyObj('UserService', ['getAll', 'delete']);
    routerMock  = jasmine.createSpyObj('Router', ['navigate']);

    userSvcMock.getAll.and.returnValue(of([mockUser1, mockUser2]));

    TestBed.configureTestingModule({
      imports: [UserManagementComponent],
      providers: [
        { provide: UserService, useValue: userSvcMock },
        { provide: Router, useValue: routerMock }
      ]
    });
  });

  it('filters users by search term', () => {
    const fixture = TestBed.createComponent(UserManagementComponent);
    fixture.detectChanges();

    fixture.componentInstance.onFilterChange('alice');
    fixture.detectChanges();

    expect(fixture.componentInstance.filteredUsers()).toHaveSize(1);
    expect(fixture.componentInstance.filteredUsers()[0].name).toBe('Alice');
  });

  it('navigates to user profile on selection', () => {
    const fixture = TestBed.createComponent(UserManagementComponent);
    fixture.detectChanges();

    fixture.componentInstance.onUserSelect(mockUser1);

    expect(routerMock.navigate).toHaveBeenCalledWith(['/users', mockUser1.id]);
  });
});

Enter fullscreen mode Exit fullscreen mode


Team Scalability and Cognitive Load {#team-scalability}

The ultimate argument for this architecture isn't technical — it's human.

Junior Developers Are Safe

When a junior developer needs to make a change to a UI component, they know the rule: change the template, the computed(), maybe add an input. They don't need to understand the service layer. They can't accidentally break business logic they weren't touching.

The architecture creates a zone of safety for less experienced developers.

Senior Developers Stay Productive

When a senior developer needs to refactor the orchestration layer, they know the presentational components are stable contracts. They can change how state is derived, where it comes from, how events are processed — without touching a single template file.

The architecture reduces the surface area of every change.

Code Reviews Become Faster

When a reviewer sees an import of AuthService in a file under /ui, they know immediately it's wrong. They don't need to understand the full feature context. The violation is visible by convention.

Fast, correct code reviews are a competitive advantage.

Onboarding Accelerates

New team members can be productive in presentational components within their first week. They don't need to understand NgRx, service architecture, or routing until they're ready to work on smart components. The cognitive progression is intentional and gradual.


The Contrarian Take {#contrarian-take}

I'll end with the opinion that starts the best architecture debates:

"The Smart vs Presentational debate was never about components. It was always about architecture boundaries."

Every team that argues against this pattern falls into one of three camps:

Camp 1: "Signals made it irrelevant." Wrong. Signals made it cleaner. The need to separate rendering from orchestration is more important in a Signals-era app, because signal-based reactivity makes it easy to put logic anywhere. The discipline is what prevents signal spaghetti.

Camp 2: "It's over-engineering for small apps." True. For a 3-developer startup shipping an MVP, strict separation is premature. For a 15+ developer team on a 2-year-old app, it's survival infrastructure. Know your context.

Camp 3: "We use NgRx so we already have separation." NgRx doesn't enforce component boundaries. It gives you a place for global state — but nothing stops a component from injecting a selector and also having local service logic and rendering complex UI. You can have NgRx and completely violated component boundaries at the same time.

The pattern isn't about tools. It's about intentional, enforced architectural decisions that make large systems maintainable by human beings who didn't write the original code.

That's what architecture is for.


Summary: The 2026 Mental Model {#summary}

Here's the complete mental model in one place:

Presentational Component Smart Component / Feature Shell
Purpose Render UI from inputs Orchestrate a feature flow
Services Never injected Injected (data, router, analytics)
State Local UI signal() only Feature signal(), computed(), toSignal()
Template Raw HTML + data binding Only component calls
Testing Zero mocks Mocked services
Folder /ui Feature root
Reuse High — across features Low — feature-specific
Contract Inputs + Outputs Business event handlers
Change detection OnPush (always) OnPush (recommended)

The Four Rules

  1. Presentational components render. Smart components coordinate.
  2. Component boundaries are contracts, not folder names.
  3. State ownership should be explicit, not accidental.
  4. Predictable components scale teams.

The One Question to Ask

Before writing any new Angular component, ask:

"Is this component rendering data, or is it deciding what data to render?"

If it's rendering → presentational, inputs and outputs only.
If it's deciding → smart, inject what you need, own the state.

That question, asked consistently, is worth more than any tool, library, or framework feature.


Found this useful? Save it before your next frontend refactor.

💬 Discussion: What's the biggest Angular component anti-pattern you still see in production? Drop it in the comments.


Further Reading


📌 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.