Table of Contents
- Follow the Official Angular Style Guide
- Modularize with Feature and Shared Modules
- Optimize Component Design: Smart vs. Presentational
- Leverage Strong Typing with Interfaces and Types
- Unsubscribe Properly to Avoid Memory Leaks
- Keep Business Logic in Services, Not Components
- Simplify Templates: Avoid Complex Logic
- Optimize Change Detection with OnPush Strategy
- Write Comprehensive, Meaningful Tests
- Use Angular CLI and Tooling for Consistency
1. Follow the Official Angular Style Guide
The Angular Style Guide is the gold standard for writing consistent, readable Angular code. It’s maintained by the Angular team and covers naming conventions, file structure, component design, and more. Adhering to it ensures your code aligns with community best practices and makes collaboration无缝 (seamless).
Key Practices:
- Naming Conventions: Use descriptive names with consistent prefixes. For example:
- Components:
user-profile.component.ts(feature +.component.ts). - Services:
user.service.ts(feature +.service.ts). - Directives:
highlight.directive.ts(behavior +.directive.ts). - Pipes:
truncate.pipe.ts(function +.pipe.ts).
- Components:
- File Structure: Organize files by feature, not type. For a
userfeature:src/ └── app/ └── user/ ├── user.component.ts ├── user.component.html ├── user.service.ts ├── user.model.ts (interface) └── user.module.ts - Component Selectors: Use a unique prefix (e.g.,
app-for your app) to avoid conflicts with standard HTML elements:@Component({ selector: 'app-user-profile', // Good: unique prefix // ... })
2. Modularize with Feature and Shared Modules
Angular’s module system (NgModule) is designed to promote separation of concerns. Avoid dumping all code into a single AppModule—instead, split your app into feature modules, shared modules, and a core module.
Feature Modules:
Encapsulate functionality for a specific feature (e.g., UserModule, DashboardModule). They can be lazy-loaded to improve app performance by loading code only when needed.
Example UserModule:
@NgModule({
declarations: [UserComponent, UserProfileComponent],
imports: [CommonModule, UserRoutingModule],
exports: [] // Export only if used outside the feature
})
export class UserModule { }
Shared Modules:
Reuse common components, directives, and pipes across the app (e.g., ButtonComponent, TooltipDirective, DateFormatPipe). Import SharedModule into feature modules that need these utilities.
Example SharedModule:
@NgModule({
declarations: [ButtonComponent, DateFormatPipe],
imports: [CommonModule],
exports: [ButtonComponent, DateFormatPipe] // Export to share
})
export class SharedModule { }
Core Module:
Contains singleton services (e.g., AuthService), guards, and interceptors used app-wide. Import it only once in AppModule to avoid duplicate service instances.
3. Optimize Component Design: Smart vs. Presentational
Components should have a single responsibility. Split them into smart (container) components and presentational (dumb) components to keep code clean and reusable.
Smart Components:
- Handle data fetching, state management, and business logic.
- Use services to interact with APIs or state.
- Do not contain styling (or minimal styling).
Example: UserListContainerComponent (smart)
@Component({
selector: 'app-user-list-container',
template: `<app-user-list [users]="users" (userSelected)="onUserSelected($event)"></app-user-list>`
})
export class UserListContainerComponent implements OnInit {
users: User[] = [];
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.getUsers().subscribe(users => this.users = users);
}
onUserSelected(user: User): void {
// Handle user selection logic
}
}
Presentational Components:
- Are reusable and focused on UI rendering.
- Take inputs (
@Input()) and emit outputs (@Output()). - Contain styling and template logic.
Example: UserListComponent (presentational)
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users" (click)="selectUser(user)">
{{ user.name }}
</li>
</ul>
`,
styles: [`li { cursor: pointer; }`]
})
export class UserListComponent {
@Input() users: User[] = [];
@Output() userSelected = new EventEmitter<User>();
selectUser(user: User): void {
this.userSelected.emit(user);
}
}
4. Leverage Strong Typing with Interfaces and Types
TypeScript is Angular’s superpower—use it! Avoid any like the plague, as it undermines type safety and defeats the purpose of TypeScript. Instead, define interfaces or types for data models, props, and service responses.
Benefits:
- Catches errors at compile time (e.g., typos in property names).
- Improves code readability (developers know exactly what data to expect).
- Enables autocompletion in IDEs.
Example: Define a User interface
// user.model.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest'; // Use union type for specific values
}
Use it in a service:
@Injectable({ providedIn: 'root' })
export class UserService {
getUsers(): Observable<User[]> { // Return type is explicit
return this.http.get<User[]>('https://api.example.com/users');
}
}
Avoid any:
// Bad: No type safety
getUsers(): Observable<any[]> { ... }
// Good: Strongly typed
getUsers(): Observable<User[]> { ... }
5. Unsubscribe Properly to Avoid Memory Leaks
Angular’s Observable subscriptions can cause memory leaks if not cleaned up. Forgetting to unsubscribe leaves subscriptions active even after a component is destroyed, leading to unnecessary API calls or state updates.
Best Ways to Unsubscribe:
-
Async Pipe: The easiest way! Automatically unsubscribes when the component is destroyed.
<!-- In template --> <div *ngIf="users$ | async as users"> <app-user-list [users]="users"></app-user-list> </div>// In component users$: Observable<User[]> = this.userService.getUsers(); // No manual subscribe! -
takeUntil + ngOnDestroy: Use for multiple subscriptions. Create a
destroy$subject that emits when the component is destroyed.export class UserComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); ngOnInit(): void { this.userService.getUsers() .pipe(takeUntil(this.destroy$)) // Unsubscribe when destroy$ emits .subscribe(users => this.users = users); this.authService.isLoggedIn$ .pipe(takeUntil(this.destroy$)) .subscribe(isLoggedIn => this.isLoggedIn = isLoggedIn); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } } -
take/first Operators: Use when you only need the first emission (e.g., a single API call):
this.userService.getUsers().pipe(take(1)).subscribe(users => ...);
6. Keep Business Logic in Services, Not Components
Components should focus on UI rendering and user interactions—not business logic, API calls, or state management. Move these responsibilities to services (singletons by default) to promote reusability and testability.
Why?
- Services are injectable and can be shared across components.
- Logic in services is easier to test (no need to render a component).
- Components stay lightweight and focused.
Example: Bad vs. Good
// Bad: API call in component
@Component({ ... })
export class UserComponent {
users: User[] = [];
ngOnInit(): void {
// Logic belongs in a service!
fetch('https://api.example.com/users')
.then(res => res.json())
.then(users => this.users = users);
}
}
// Good: API call in service
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) { }
getUsers(): Observable<User[]> {
return this.http.get<User[]>('https://api.example.com/users');
}
}
// Component uses the service
@Component({ ... })
export class UserComponent {
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.getUsers().subscribe(users => this.users = users);
}
}
7. Simplify Templates: Avoid Complex Logic
Templates should be declarative and easy to read. Avoid complex expressions, conditionals, or calculations directly in templates—move that logic to the component class.
Tips:
-
Use Getters for Computed Values:
// Component get fullName(): string { return `${this.user.firstName} ${this.user.lastName}`; }<!-- Template --> <div>{{ fullName }}</div> <!-- Clean! --> -
Avoid Nested Conditionals: Use
*ngIfwithelseorng-containerfor readability:<!-- Bad: Hard to follow --> <div *ngIf="user && user.isAdmin && user.isActive">Admin Panel</div> <!-- Good: Simplified with ng-container --> <ng-container *ngIf="user; else noUser"> <div *ngIf="user.isActive && user.isAdmin">Admin Panel</div> </ng-container> <ng-template #noUser>Please log in.</ng-template> -
Use Pipes for Transformations: Create pure pipes for reusable logic (e.g., formatting dates, truncating text):
@Pipe({ name: 'truncate' }) export class TruncatePipe implements PipeTransform { transform(value: string, maxLength: number = 10): string { return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value; } }<div>{{ user.bio | truncate:20 }}</div> <!-- "Hello world! This is a ..." -->
8. Optimize Change Detection with OnPush Strategy
Angular’s default change detection strategy checks all components on every event (e.g., clicks, HTTP responses). For large apps, this can slow things down. Use the OnPush strategy to limit checks and boost performance.
How OnPush Works:
A component with changeDetection: ChangeDetectionStrategy.OnPush only updates when:
- An input reference changes (not just the value).
- An event is emitted from the component (e.g., button click).
- You manually trigger change detection with
ChangeDetectorRef.
Example:
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush, // Enable OnPush
template: `{{ user.name }}`
})
export class UserCardComponent {
@Input() user: User;
}
To trigger updates with OnPush, pass immutable data (e.g., return new arrays/objects instead of mutating existing ones):
// Bad: Mutation (OnPush won't detect)
this.users.push(newUser);
// Good: Immutable update (new array reference)
this.users = [...this.users, newUser];
9. Write Comprehensive, Meaningful Tests
Clean code is testable code. Angular’s testing utilities (Jasmine/Karma for unit tests, Cypress/Playwright for E2E) help ensure your code works as expected and prevents regressions.
What to Test:
-
Services: Test API calls, business logic, and edge cases.
describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [UserService] }); service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); }); it('should fetch users', () => { const mockUsers: User[] = [{ id: 1, name: 'John' }]; service.getUsers().subscribe(users => { expect(users).toEqual(mockUsers); }); const req = httpMock.expectOne('https://api.example.com/users'); expect(req.request.method).toBe('GET'); req.flush(mockUsers); }); }); -
Components: Test inputs, outputs, and template rendering.
-
Pipes/Directives: Test their transform logic or behavior.
Aim for high test coverage, but prioritize critical paths over trivial code.
10. Use Angular CLI and Tooling for Consistency
The Angular CLI (ng) is your best friend for scaffolding, building, and maintaining projects. It enforces best practices out of the box and integrates with tools to keep code clean.
Essential CLI Commands:
ng new my-app: Create a new project with linting, testing, and routing.ng generate component user: Scaffold a component with files and module updates.ng generate service user: Generate a service with dependency injection.ng lint: Run ESLint to catch style/error issues.ng format: Auto-format code with Prettier (if configured).
Tooling:
- ESLint + Prettier: Enforce code style and formatting. Add to
angular.json:"architect": { "lint": { "builder": "@angular-eslint/builder:lint", "options": { "lintFilePatterns": ["src/**/*.ts"] } } } - Husky + lint-staged: Run linting/formatting before commits to keep the repo clean.
- Angular DevTools: Debug change detection, component hierarchies, and state in the browser.
Conclusion
Writing clean Angular code isn’t about perfection—it’s about consistency, readability, and maintainability. By following these tips—from modularizing with feature modules to unsubscribing properly—you’ll build apps that are easier to debug, scale, and collaborate on.
Remember: Clean code is a habit. Start small, iterate, and always refer to the Angular Style Guide and TypeScript docs for guidance.