.claude/skills/angular-best-practices/SKILL.md
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.
npx skillsauth add tranhieutt/software_development_department angular-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
ngOnDestroy — use takeUntilDestroyed() (Angular 16+) or Subject + takeUntilChangeDetectionStrategy.OnPush: component only updates when input reference changes or async pipe emits — use for all leaf componentstrackBy is mandatory on *ngFor with dynamic lists — without it, every change re-renders all DOM nodesasync pipe auto-unsubscribes — prefer it over manual subscription in templates@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);
}
@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 }]
);
}
}
// 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]))
// 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"
// 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(); }
}
// 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 {}
| Pitfall | Fix |
|---|---|
| Memory leak from unsubscribed Observable | Use takeUntilDestroyed() or async pipe |
| ExpressionChangedAfterChecked error | Defer with afterNextRender() or move to signals |
| Heavy computation in template | Move to computed() signal or pipe(map(...)) |
| *ngIf with async pipe fetches twice | Use as syntax: *ngIf="data$ \| async as data" |
| Zone.js performance in loops | Use ChangeDetectionStrategy.OnPush + signals |
testing
Generates high-fidelity architecture diagrams, sequence flows, and component maps for SDD projects. Use when finalizing a design phase, documenting system architecture, or visualizing agentic workflows. Default style: Style 6 (Claude Official).
data-ai
Provides vector database and semantic search patterns for Pinecone, Weaviate, Qdrant, Milvus, and pgvector in RAG and recommendation systems. Use when implementing vector search or when the user mentions vector database, semantic search, embeddings, or similarity search.
development
Updates docs/technical/CODEMAP.md by scanning the current codebase structure. Run after a significant feature merge, refactor, or when CODEMAP feels stale.
development
Unlocks the codebase after a release freeze or incident freeze period to resume normal development. Use when a freeze period ends or when the user mentions unfreezing or lifting the code freeze.