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');
}
}
6. Services with Component Logic
The Anti-pattern
Creating services that contain component-specific logic or directly manipulate the UI:
@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:
@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);
}
}
@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:
// 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:
@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:
@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:
- Massive Components: Break down large components into smaller, focused ones with single responsibilities.
- Logic in Templates: Keep templates simple by moving business logic to the component class.
- Not Using OnPush Change Detection: Optimize performance with OnPush for presentational components.
- Not Unsubscribing from Observables: Always clean up subscriptions to prevent memory leaks.
- Manipulating the DOM Directly: Use Angular's bindings and Renderer2 instead of direct DOM manipulation.
- Services with Component Logic: Keep services focused on data and business logic, not UI concerns.
- 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
- Official Angular Style Guide
- Angular Performance Optimization Guide
- RxJS Best Practices
- Angular University Blog
- Nx Blog on Angular Architecture
Exercises
- Take an existing large component in your application and refactor it into smaller, more focused components.
- Find instances of complex logic in your templates and move them to component methods.
- Add OnPush change detection to your presentational components and measure the performance improvement.
- Review your components for unhandled subscriptions and fix them using the takeUntil pattern.
- 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! :)