Angular Anti-patterns
Introduction
As you build Angular applications, it's just as important to know what practices to avoid as it is to know the best practices to follow. Angular anti-patterns are common but problematic implementation approaches that lead to code that's difficult to maintain, inefficient, or prone to bugs.
In this guide, we'll explore several common Angular anti-patterns and provide alternatives that align with best practices. Understanding these anti-patterns will help you write cleaner, more maintainable, and more efficient Angular code.
What are Anti-patterns?
Anti-patterns are common approaches to recurring problems that seem like good solutions initially but ultimately create more problems than they solve. In Angular development, these can lead to:
- Poor application performance
- Difficult-to-maintain code
- Memory leaks
- Testing difficulties
- Confusing component hierarchies
Let's examine the most common Angular anti-patterns and how to avoid them.
1. Massive Components
The Anti-pattern
Creating components that do too much is a frequent mistake in Angular applications. These "god components" have too many responsibilities, contain excessive amounts of code, and are difficult to understand and maintain.
// massive-component.component.ts
@Component({
  selector: 'app-massive-component',
  template: `
    <div>
      <h1>User Dashboard</h1>
      
      <!-- User profile -->
      <div>
        <h2>Profile</h2>
        <img [src]="user.avatar" />
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <!-- Lots more profile UI -->
      </div>
      
      <!-- User analytics -->
      <div>
        <h2>Analytics</h2>
        <canvas #analyticsChart></canvas>
        <!-- Complex chart initialization code in component -->
      </div>
      
      <!-- Messages -->
      <div>
        <h2>Messages ({{ messages.length }})</h2>
        <div *ngFor="let message of messages">
          <!-- Message UI -->
        </div>
        <form (submit)="sendMessage()">
          <!-- Form inputs -->
        </form>
      </div>
      
      <!-- Settings -->
      <div>
        <h2>Settings</h2>
        <!-- Lots of settings UI -->
      </div>
    </div>
  `
})
export class MassiveComponent implements OnInit {
  user: User;
  messages: Message[] = [];
  analyticsData: any[] = [];
  settings: UserSettings;
  
  @ViewChild('analyticsChart') chartCanvas: ElementRef;
  
  constructor(
    private userService: UserService,
    private messageService: MessageService,
    private analyticsService: AnalyticsService,
    private settingsService: SettingsService,
    private chartService: ChartService
  ) {}
  
  ngOnInit() {
    this.loadUserProfile();
    this.loadMessages();
    this.loadAnalytics();
    this.loadSettings();
  }
  
  // Many methods for different responsibilities
  loadUserProfile() { /* ... */ }
  loadMessages() { /* ... */ }
  sendMessage() { /* ... */ }
  loadAnalytics() { /* ... */ }
  loadSettings() { /* ... */ }
  updateSettings() { /* ... */ }
  
  // Component has too many responsibilities
}
The Solution
Break the large component into smaller, focused components with single responsibilities:
// user-dashboard.component.ts
@Component({
  selector: 'app-user-dashboard',
  template: `
    <div>
      <h1>User Dashboard</h1>
      <app-user-profile [user]="user"></app-user-profile>
      <app-user-analytics [userId]="user.id"></app-user-analytics>
      <app-user-messages [userId]="user.id"></app-user-messages>
      <app-user-settings [userId]="user.id"></app-user-settings>
    </div>
  `
})
export class UserDashboardComponent implements OnInit {
  user: User;
  
  constructor(private userService: UserService) {}
  
  ngOnInit() {
    this.loadUserProfile();
  }
  
  loadUserProfile() {
    this.userService.getUser().subscribe(user => this.user = user);
  }
}
Each child component now has a single responsibility, making the code more maintainable, easier to test, and reusable.
2. Logic in Templates
The Anti-pattern
Placing complex logic directly in templates makes your application harder to test and maintain.
<!-- complex-template.component.html -->
<div>
  <h2>Products</h2>
  <div *ngFor="let product of products">
    <h3>{{ product.name }}</h3>
    <p>Price: {{ product.price * (1 - discount) | currency }}</p>
    <p>
      Status: 
      <span *ngIf="product.stock > 10" class="in-stock">In Stock</span>
      <span *ngIf="product.stock <= 10 && product.stock > 0" class="low-stock">Low Stock</span>
      <span *ngIf="product.stock === 0" class="out-of-stock">Out of Stock</span>
    </p>
    <button 
      [disabled]="product.stock === 0 || !userService.isLoggedIn() || cartService.isProductInCart(product.id)"
      (click)="addToCart(product)">
      {{ product.stock === 0 ? 'Out of Stock' : 
         cartService.isProductInCart(product.id) ? 'In Cart' : 'Add to Cart' }}
    </button>
  </div>
</div>
The Solution
Move business logic to the component class, and use simple expressions in templates:
// product-list.component.ts
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
  products: Product[] = [];
  discount = 0.1;
  
  constructor(
    private cartService: CartService,
    private userService: UserService
  ) {}
  
  getDiscountedPrice(product: Product): number {
    return product.price * (1 - this.discount);
  }
  
  getStockStatus(product: Product): string {
    if (product.stock > 10) return 'In Stock';
    if (product.stock > 0) return 'Low Stock';
    return 'Out of Stock';
  }
  
  getStockStatusClass(product: Product): string {
    if (product.stock > 10) return 'in-stock';
    if (product.stock > 0) return 'low-stock';
    return 'out-of-stock';
  }
  
  isAddToCartDisabled(product: Product): boolean {
    return product.stock === 0 || 
           !this.userService.isLoggedIn() || 
           this.cartService.isProductInCart(product.id);
  }
  
  getButtonText(product: Product): string {
    if (product.stock === 0) return 'Out of Stock';
    if (this.cartService.isProductInCart(product.id)) return 'In Cart';
    return 'Add to Cart';
  }
  
  addToCart(product: Product): void {
    this.cartService.addProduct(product);
  }
}
<!-- product-list.component.html -->
<div>
  <h2>Products</h2>
  <div *ngFor="let product of products">
    <h3>{{ product.name }}</h3>
    <p>Price: {{ getDiscountedPrice(product) | currency }}</p>
    <p>
      Status: 
      <span [class]="getStockStatusClass(product)">{{ getStockStatus(product) }}</span>
    </p>
    <button 
      [disabled]="isAddToCartDisabled(product)"
      (click)="addToCart(product)">
      {{ getButtonText(product) }}
    </button>
  </div>
</div>
This approach makes your code more testable and easier to maintain.
3. Not Using OnPush Change Detection
The Anti-pattern
Using default change detection for all components can lead to unnecessary rendering and performance issues, especially in large applications.
@Component({
  selector: 'app-item-list',
  template: `
    <div *ngFor="let item of items">
      {{ item.name }}
    </div>
  `
})
export class ItemListComponent {
  @Input() items: Item[];
}
The Solution
Use OnPush change detection for presentational components that only depend on their inputs:
@Component({
  selector: 'app-item-list',
  template: `
    <div *ngFor="let item of items">
      {{ item.name }}
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemListComponent {
  @Input() items: Item[];
}
This tells Angular to only check the component when:
- Input references change
- An event originates from the component or its children
- Change detection is triggered manually
- An observable the component subscribes to with the async pipe emits a new value
4. Not Unsubscribing from Observables
The Anti-pattern
Failing to unsubscribe from observables can cause memory leaks, especially in components that might be created and destroyed frequently.
@Component({
  selector: 'app-data-monitor',
  template: `<div>{{ data | json }}</div>`
})
export class DataMonitorComponent implements OnInit {
  data: any;
  
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    // Problem: This subscription is never cleaned up
    this.dataService.getDataStream().subscribe(data => {
      this.data = data;
    });
  }
}
The Solution
Always unsubscribe from observables when the component is destroyed:
@Component({
  selector: 'app-data-monitor',
  template: `<div>{{ data | json }}</div>`
})
export class DataMonitorComponent implements OnInit, OnDestroy {
  data: any;
  private subscription: Subscription;
  
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    this.subscription = this.dataService.getDataStream().subscribe(data => {
      this.data = data;
    });
  }
  
  ngOnDestroy() {
    // Clean up to prevent memory leaks
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}
Even better, use the takeUntil pattern for multiple subscriptions:
@Component({
  selector: 'app-data-monitor',
  template: `<div>{{ data | json }}</div>`
})
export class DataMonitorComponent implements OnInit, OnDestroy {
  data: any;
  private destroy$ = new Subject<void>();
  
  constructor(private dataService: DataService) {}
  
  ngOnInit() {
    this.dataService.getDataStream()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => {
        this.data = data;
      });
      
    // Additional subscriptions
    this.dataService.getAnotherStream()
      .pipe(takeUntil(this.destroy$))
      .subscribe(result => {
        // handle result
      });
  }
  
  ngOnDestroy() {
    // Clean up all subscriptions at once
    this.destroy$.next();
    this.destroy$.complete();
  }
}
5. Manipulating the DOM Directly
The Anti-pattern
Directly manipulating the DOM with native JavaScript methods or jQuery can lead to code that's hard to test and potentially conflicts with Angular's change detection.
@Component({
  selector: 'app-message-box',
  template: `<div #messageBox></div>`
})
export class MessageBoxComponent implements AfterViewInit {
  @ViewChild('messageBox') messageBoxElement: ElementRef;
  
  constructor() {}
  
  ngAfterViewInit() {
    // Anti-pattern: Direct DOM manipulation
    this.messageBoxElement.nativeElement.innerHTML = '<p>New message!</p>';
    this.messageBoxElement.nativeElement.style.backgroundColor = 'yellow';
    
    setTimeout(() => {
      this.messageBoxElement.nativeElement.style.backgroundColor = 'white';
    }, 2000);
  }
}
The Solution
Use Angular's binding, directives, and renderer for DOM manipulation:
@Component({
  selector: 'app-message-box',
  template: `
    <div 
      [innerHTML]="messageHtml"
      [style.background-color]="backgroundColor">
    </div>
  `
})
export class MessageBoxComponent implements OnInit {
  messageHtml = '<p>New message!</p>';
  backgroundColor = 'yellow';
  
  constructor(private renderer: Renderer2) {}
  
  ngOnInit() {
    setTimeout(() => {
      this.backgroundColor = 'white';
    }, 2000);
  }
  
  // If you must manipulate the DOM directly, use Angular's Renderer2
  updateElementWithRenderer(element: ElementRef) {
    this.renderer.setStyle(element.nativeElement, 'color', 'blue');
    this.renderer.addClass(element.nativeElement, 'highlight');
  }
}