Skip to main content

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.

typescript
// 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:

typescript
// 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.

html
<!-- 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:

typescript
// 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);
}
}
html
<!-- 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.

typescript
@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:

typescript
@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:

  1. Input references change
  2. An event originates from the component or its children
  3. Change detection is triggered manually
  4. 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.

typescript
@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:

typescript
@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:

typescript
@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.

typescript
@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:

typescript
@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');
}
}

6. Services with Component Logic

The Anti-pattern

Creating services that contain component-specific logic or directly manipulate the UI:

typescript
@Injectable({ providedIn: 'root' })
export class BadUIService {
// Anti-pattern: Service contains UI-specific logic
showUserProfile(user: User) {
const profileElement = document.getElementById('user-profile');
if (profileElement) {
profileElement.innerHTML = `
<h2>${user.name}</h2>
<p>${user.email}</p>
<img src="${user.avatar}" alt="${user.name}" />
`;
profileElement.style.display = 'block';
}
}

hideUserProfile() {
const profileElement = document.getElementById('user-profile');
if (profileElement) {
profileElement.style.display = 'none';
}
}
}

The Solution

Keep services focused on data and business logic, leaving UI concerns to components:

typescript
@Injectable({ providedIn: 'root' })
export class UserService {
// Good: Service focuses on data operations
getUser(userId: string): Observable<User> {
return this.http.get<User>(`/api/users/${userId}`);
}

updateUser(user: User): Observable<User> {
return this.http.put<User>(`/api/users/${user.id}`, user);
}
}
typescript
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="showProfile">
<h2>{{ user?.name }}</h2>
<p>{{ user?.email }}</p>
<img [src]="user?.avatar" [alt]="user?.name" />
<button (click)="hideProfile()">Close</button>
</div>
`
})
export class UserProfileComponent {
@Input() userId: string;
user: User | null = null;
showProfile = false;

constructor(private userService: UserService) {}

showUserProfile() {
this.userService.getUser(this.userId)
.subscribe(user => {
this.user = user;
this.showProfile = true;
});
}

hideProfile() {
this.showProfile = false;
}
}

7. Over-using NgRx/Redux for Simple Applications

The Anti-pattern

Using complex state management solutions like NgRx for small applications that don't need them:

typescript
// In a simple todo-list app
export interface State {
todos: Todo[];
loading: boolean;
error: string | null;
}

export const initialState: State = {
todos: [],
loading: false,
error: null
};

// Action types
export const LOAD_TODOS = '[Todos] Load';
export const LOAD_TODOS_SUCCESS = '[Todos] Load Success';
export const LOAD_TODOS_FAILURE = '[Todos] Load Failure';
export const ADD_TODO = '[Todos] Add';
// ... many more actions

// Reducer
export function todosReducer(state = initialState, action: TodosActions): State {
switch (action.type) {
case LOAD_TODOS:
return { ...state, loading: true };
case LOAD_TODOS_SUCCESS:
return { ...state, todos: action.payload, loading: false };
case LOAD_TODOS_FAILURE:
return { ...state, error: action.payload, loading: false };
// ... many more cases
default:
return state;
}
}

// Effects, selectors, etc...

The Solution

Choose the right state management approach for your application's complexity:

For simple applications:

typescript
@Injectable({ providedIn: 'root' })
export class TodoService {
private todos$ = new BehaviorSubject<Todo[]>([]);

getTodos(): Observable<Todo[]> {
return this.todos$.asObservable();
}

loadTodos(): void {
this.http.get<Todo[]>('/api/todos')
.subscribe(
todos => this.todos$.next(todos),
error => console.error('Failed to load todos', error)
);
}

addTodo(todo: Todo): void {
this.http.post<Todo>('/api/todos', todo)
.subscribe(newTodo => {
const currentTodos = this.todos$.getValue();
this.todos$.next([...currentTodos, newTodo]);
});
}
}

Use this service directly in your components:

typescript
@Component({
selector: 'app-todo-list',
template: `
<div>
<h2>Todo List</h2>
<ul>
<li *ngFor="let todo of todos$ | async">
{{ todo.title }}
</li>
</ul>
</div>
`
})
export class TodoListComponent implements OnInit {
todos$: Observable<Todo[]>;

constructor(private todoService: TodoService) {
this.todos$ = this.todoService.getTodos();
}

ngOnInit() {
this.todoService.loadTodos();
}
}

Only move to NgRx when your application has:

  • Complex state that's shared across many components
  • Multiple sources of truth
  • Complex user flows and state transitions
  • A need for powerful debugging tools

Summary

In this guide, we've covered seven common Angular anti-patterns and their solutions:

  1. Massive Components: Break down large components into smaller, focused ones with single responsibilities.
  2. Logic in Templates: Keep templates simple by moving business logic to the component class.
  3. Not Using OnPush Change Detection: Optimize performance with OnPush for presentational components.
  4. Not Unsubscribing from Observables: Always clean up subscriptions to prevent memory leaks.
  5. Manipulating the DOM Directly: Use Angular's bindings and Renderer2 instead of direct DOM manipulation.
  6. Services with Component Logic: Keep services focused on data and business logic, not UI concerns.
  7. Over-using NgRx/Redux: Choose the right state management approach based on your application's complexity.

By avoiding these anti-patterns, you'll write more maintainable, efficient, and robust Angular applications.

Additional Resources

Exercises

  1. Take an existing large component in your application and refactor it into smaller, more focused components.
  2. Find instances of complex logic in your templates and move them to component methods.
  3. Add OnPush change detection to your presentational components and measure the performance improvement.
  4. Review your components for unhandled subscriptions and fix them using the takeUntil pattern.
  5. Refactor any direct DOM manipulation code to use Angular's binding system and Renderer2.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)