Software_development_department angular-best-practices
Provides Angular best practices for components, modules, services, and reactive patterns. Use when working with Angular TypeScript files, component templates, NgModules, RxJS observables, or when the user mentions Angular, ng, or Angular CLI.
install
source · Clone the upstream repo
git clone https://github.com/tranhieutt/software_development_department
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tranhieutt/software_development_department "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/angular-best-practices" ~/.claude/skills/tranhieutt-software-development-department-angular-best-practices && rm -rf "$T"
manifest:
.claude/skills/angular-best-practices/SKILL.mdsource content
Angular Best Practices
Critical rules (non-obvious)
- Always unsubscribe from Observables in
— usengOnDestroy
(Angular 16+) ortakeUntilDestroyed()
+SubjecttakeUntil
: component only updates when input reference changes or async pipe emits — use for all leaf componentsChangeDetectionStrategy.OnPush- Never mutate input objects/arrays: OnPush won't detect mutation; create new reference instead
is mandatory ontrackBy
with dynamic lists — without it, every change re-renders all DOM nodes*ngFor
pipe auto-unsubscribes — prefer it over manual subscription in templatesasync
Component with OnPush + signals (Angular 17+)
@Component({ selector: "app-product-list", changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for (product of products(); track product.id) { <app-product-card [product]="product" /> } @if (loading()) { <app-spinner /> } `, }) export class ProductListComponent { products = input.required<Product[]>(); loading = input(false); // Computed signal total = computed(() => this.products().length); }
Service with signals store pattern
@Injectable({ providedIn: "root" }) export class CartService { private _items = signal<CartItem[]>([]); items = this._items.asReadonly(); total = computed(() => this._items().reduce((sum, i) => sum + i.price * i.qty, 0)); addItem(item: CartItem) { this._items.update(items => items.some(i => i.id === item.id) ? items.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i) : [...items, { ...item, qty: 1 }] ); } }
HTTP with interceptors
// auth interceptor export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthService).token(); if (!token) return next(req); return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })).pipe( catchError(err => { if (err.status === 401) inject(Router).navigate(["/login"]); return throwError(() => err); }) ); }; // Register in app.config.ts provideHttpClient(withInterceptors([authInterceptor]))
RxJS: key operators (non-obvious behavior)
// switchMap: cancels previous — good for search, bad for saves search$.pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.api.search(term)) // cancels in-flight request on new input ) // exhaustMap: ignores new while processing — good for login button loginClick$.pipe( exhaustMap(() => this.auth.login(credentials)) // prevents double-submit ) // mergeMap: parallel — good for independent operations ids$.pipe(mergeMap(id => this.api.fetch(id), 3)) // 3 concurrent max // combineLatest vs withLatestFrom: // combineLatest: emits when ANY source emits // withLatestFrom: emits only when primary source emits, takes latest from secondary primary$.pipe(withLatestFrom(secondary$)) // common for "take latest filter value on button click"
Auto-unsubscribe pattern
// Angular 16+ (preferred) @Component({...}) export class MyComponent { private destroyRef = inject(DestroyRef); ngOnInit() { this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...); } } // Before Angular 16 export class MyComponent implements OnDestroy { private destroy$ = new Subject<void>(); ngOnInit() { this.data$.pipe(takeUntil(this.destroy$)).subscribe(...); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } }
Lazy loading + standalone components
// app.routes.ts export const routes: Routes = [ { path: "admin", loadChildren: () => import("./admin/admin.routes").then(m => m.ADMIN_ROUTES), canMatch: [adminGuard], }, ]; // Standalone component (Angular 15+) @Component({ standalone: true, imports: [CommonModule, RouterModule, ReactiveFormsModule], template: `...`, }) export class ProfileComponent {}
Common pitfalls
| Pitfall | Fix |
|---|---|
| Memory leak from unsubscribed Observable | Use or pipe |
error | Defer with or move to signals |
| Heavy computation in template | Move to signal or |
with pipe fetches twice | Use syntax: |
| Zone.js performance in loops | Use + signals |