1. Embrace Standalone Components
Simplify your architecture by using standalone components, pipes, and directives. This is the default in modern Angular and reduces the boilerplate of NgModules.// user-profile.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-profile',
standalone: true, // Mark component as standalone
imports: [CommonModule], // Import dependencies directly
template: `<p>User Profile</p>`
})
export class UserProfileComponent {}
2. Use the Built-in Control Flow (@if, @For)
The new built-in control flow is more performant and has a cleaner, more intuitive syntax than the older *ngIf and *ngFor directives.TypeScript
// user-list.component.ts
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@For (user of users; track user.id) {
<div>{{ user.name }}</div>
} @empty {
<p>No users found.</p>
}
`
})
export class UserListComponent {
users = [{id: 1, name: 'John Doe'}];
}
3. Leverage Signals for State Management
Use Angular Signals for fine-grained, efficient state management. They are the new core reactive primitive in Angular, simplifying state changes and improving performance.TypeScript
// counter.component.ts
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="increment()">Count: {{ count() }}</button>
<p>Double count is: {{ doubleCount() }}</p>
`
})
export class CounterComponent {
// Create a writable signal
count = signal(0);
// Create a computed signal that depends on 'count'
doubleCount = computed(() => this.count() * 2);
increment() {
// Update the signal's value
this.count.update(value => value + 1);
}
}
4. Use OnPush Change Detection
Improve performance by reducing unnecessary change detection cycles. OnPush is a powerful strategy, especially in large applications.TypeScript
// user-card.component.ts
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';@Component({
selector: 'app-user-card',
standalone: true,
template: `<h2>{{ user.name }}</h2>`,
// Use OnPush strategy to optimize change detection
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
@Input() user: { name: string };
}
5. Use the async Pipe
The async pipe automatically subscribes to an Observable or Promise and returns the latest value. It also unsubscribes automatically when the component is destroyed, preventing memory leaks.TypeScript
// user-data.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-user-data',
standalone: true,
imports: [CommonModule],
template: `
<div>{{ user$ | async }}</div>
`
})
export class UserDataComponent {
user$: Observable<string> = of('Jane Doe');
}
6. Lazy Load with @defer
Use the @defer block to declaratively lazy load components, directives, and pipes. This improves initial page load performance by deferring the loading of non-critical content.TypeScript
// dashboard.component.ts
import { HeavyChartComponent } from './heavy-chart.component';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [HeavyChartComponent], // The component to be deferred
template: `
<h1>Dashboard</h1>
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<p>Chart is loading...</p>
} @ERROR {
<p>Failed to load chart.</p>
}
`
})
export class DashboardComponent {}
7. Use trackBy for Lists
When rendering lists, trackBy (or the track parameter in @For) helps Angular identify which items have changed, preventing the entire list from being re-rendered in the DOM.TypeScript
// product-list.component.ts
@Component({
selector: 'app-product-list',
standalone: true,
template: `
@for(product of products; track product.id) {
<div>{{ product.name }}</div>
}
`
})
export class ProductListComponent {
products = [{id: 1, name: 'Laptop'}, {id: 2, name: 'Mouse'}];
}
8. Use Strictly Typed Reactive Forms
Take advantage of strongly typed forms (stable since v14) to improve type safety, autocompletion, and developer experience.TypeScript
// profile-form.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({ /* ... */ })
export class ProfileFormComponent {
// Define types for the form controls for better safety
profileForm = new FormGroup({
name: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email])
});
}
9. Use the inject() Function
The inject() function allows for constructor-less dependency injection, offering more flexibility, especially within reusable functions and modern functional components like route guards.TypeScript
// logger.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class LoggerService {
// Use inject() for a more flexible DI pattern
private http = inject(HttpClient);
log(message: string) {
console.log(message);
// this.http.post(...);
}
}
10. Use Functional Route Guards
With the evolution of Angular, functional guards have become the standard. They are simpler, tree-shakable, and don’t require a class.TypeScript
// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';// A functional route guard is a simple, exportable function
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Redirect to login page if not authenticated
return router.parseUrl('/login');
};
11. Use Functional HTTP Interceptors
Similar to guards, HTTP interceptors can now be written as simple functions, making them more lightweight and easier to manage.TypeScript
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authToken = 'YOUR_AUTH_TOKEN';
// Clone the request and add the authorization header
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${authToken}` }
});
return next(authReq);
};
12. Use ng-content for Content Projection
Create flexible, reusable wrapper components by projecting content into them using ng-content.TypeScript
// card.component.ts
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {}
13. Leverage Environment Variables
Keep your application configuration clean and separate for different environments (development, production) using the built-in environment files.TypeScript
// environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api'
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: '[https://api.myapp.com/api](https://api.myapp.com/api)'
};
14. Avoid any Type
Leverage TypeScript’s strict typing to catch errors early, improve code readability, and make your code more self-documenting.TypeScript
// BAD: Unclear and error-prone
function processItem(item: any) {
console.log(item.name); // No type checking
}
// GOOD: Type-safe and explicit
interface User { id: number; name: string; }
function processUser(user: User) {
console.log(user.name);
}
15. Use HostBinding and HostListener
Use these decorators to manage properties and events on the host element of a component or directive without directly accessing the DOM.TypeScript
// highlight.directive.ts
import { Directive, HostBinding, HostListener, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
// Bind the host element's backgroundColor property
@HostBinding('style.backgroundColor') backgroundColor: string = '';
// Listen for mouseenter event on the host
@HostListener('mouseenter') onMouseEnter() {
this.backgroundColor = 'yellow';
}
// Listen for mouseleave event on the host
@HostListener('mouseleave') onMouseLeave() {
this.backgroundColor = '';
}
}
16. Use Aliases for Inputs/Outputs Sparingly
Stick to the original property name for @Input() and @Output() unless an alias is truly necessary. This keeps the public API of your component consistent with its internal properties.TypeScript
// BAD: Inconsistent and can cause confusion
@Input('userRecord') user: User;
@Output('userChange') userEmitter = new EventEmitter<User>();
// GOOD: Clean, simple, and predictable
@Input() user: User;
@Output() userChange = new EventEmitter<User>();
17. Use Dependency Injection (DI) Tokens
Use an InjectionToken to provide non-class dependencies like configuration objects or values.TypeScript
// app.config.ts
import { InjectionToken } from '@angular/core';
export interface AppConfig { apiUrl: string; apiKey: string; }
// Create a token to be used for dependency injection
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
18. Keep Components Lean
Components should primarily handle view logic. Delegate complex business logic, API calls, and data transformations to services to keep your components clean and focused.TypeScript
// BAD: Component is doing too much work (fat component)
@Component({ /* ... */ })
export class UserProfileComponent implements OnInit {
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('/api/user').subscribe(/* ... */);
}
}
// GOOD: Component delegates complex logic to a dedicated service
@Component({ /* ... */ })
export class UserProfileComponent implements OnInit {
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUser().subscribe(/* ... */);
}
}
19. Analyze Your Bundle Size
Regularly check what’s contributing to your application’s final bundle size to identify optimization opportunities. A great tool for this is source-map-explorer.Bash
# First, build your app with source maps
ng build --source-map
# Then, run the explorer on the generated bundles
npx source-map-explorer dist/your-app-name/browser/*.js
20. Write Meaningful Tests
Ensure your application’s reliability and make refactoring safer by writing unit tests for your logic (services, functions) and component tests for your UI interactions.TypeScript
// user.service.spec.ts
describe('UserService', () => {
it('should fetch user data correctly', () => {
// Test your service logic here
// Mock dependencies like HttpClient and assert outcomes
});
});