Angular Expert Skill
Expert-level Angular patterns for components, RxJS, state management, and performance.
Auto-Detection
This skill activates when:
-
Working with Angular projects
-
Detected angular.json or @angular/core in package.json
-
Working with *.component.ts , *.service.ts files
-
Using RxJS, NgRx, or Angular Material
- Component Best Practices
Standalone Components (Angular 17+)
// ✅ GOOD - Standalone component
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink],
template: <div class="user-card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> <a [routerLink]="['/users', user.id]">View Profile</a> </div> ,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
@Input({ required: true }) user!: User;
@Output() selected = new EventEmitter<User>();
}
Signal-Based Components (Angular 17+)
// ✅ GOOD - Signals for reactive state
@Component({
selector: 'app-counter',
standalone: true,
template: <div> <p>Count: {{ count() }}</p> <p>Double: {{ doubleCount() }}</p> <button (click)="increment()">+</button> </div> ,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); } }
Smart vs Dumb Components
// ✅ GOOD - Container (Smart) component
@Component({
selector: 'app-users-container',
standalone: true,
imports: [UserListComponent],
template: <app-user-list [users]="users()" [loading]="loading()" (userSelected)="onUserSelected($event)" /> ,
})
export class UsersContainerComponent {
private userService = inject(UserService);
users = signal<User[]>([]); loading = signal(false);
constructor() { this.loadUsers(); }
private async loadUsers() { this.loading.set(true); this.users.set(await this.userService.getUsers()); this.loading.set(false); }
onUserSelected(user: User) { this.userService.selectUser(user); } }
// ✅ GOOD - Presentational (Dumb) component
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: @if (loading) { <div class="loading">Loading...</div> } @else { <ul> @for (user of users; track user.id) { <li (click)="userSelected.emit(user)"> {{ user.name }} </li> } </ul> } ,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
@Input() users: User[] = [];
@Input() loading = false;
@Output() userSelected = new EventEmitter<User>();
}
- Services & Dependency Injection
Injectable Services
// ✅ GOOD - Service with inject() @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); private baseUrl = inject(API_BASE_URL);
getUsers(): Observable<User[]> {
return this.http.get<User[]>(${this.baseUrl}/users);
}
getUser(id: string): Observable<User> {
return this.http.get<User>(${this.baseUrl}/users/${id});
}
createUser(user: CreateUserDto): Observable<User> {
return this.http.post<User>(${this.baseUrl}/users, user);
}
}
Injection Tokens
// ✅ GOOD - Injection tokens for config export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
// In app.config.ts export const appConfig: ApplicationConfig = { providers: [ { provide: API_BASE_URL, useValue: environment.apiUrl }, ], };
- RxJS Best Practices
Declarative Streams
// ✅ GOOD - Declarative with signals @Component({...}) export class UsersComponent { private userService = inject(UserService); private route = inject(ActivatedRoute);
// Derived state from route params private userId = toSignal( this.route.paramMap.pipe(map(params => params.get('id'))) );
user = toSignal( toObservable(this.userId).pipe( filter((id): id is string => id != null), switchMap(id => this.userService.getUser(id)), ) ); }
Error Handling
// ✅ GOOD - catchError with recovery getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users').pipe( retry({ count: 3, delay: 1000 }), catchError(error => { console.error('Failed to fetch users', error); return of([]); // Return empty array on error }), ); }
// ✅ GOOD - Error handling in component @Component({...}) export class UsersComponent { users$ = this.userService.getUsers().pipe( catchError(error => { this.errorMessage.set(error.message); return EMPTY; }), );
errorMessage = signal<string | null>(null); }
Unsubscribe Patterns
// ✅ GOOD - takeUntilDestroyed @Component({...}) export class MyComponent { private destroyRef = inject(DestroyRef);
ngOnInit() { this.someObservable$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(value => { // Handle value }); } }
// ✅ GOOD - async pipe (auto-unsubscribes)
@Component({
template: @if (users$ | async; as users) { <app-user-list [users]="users" /> } ,
})
export class UsersComponent {
users$ = this.userService.getUsers();
}
- State Management (NgRx)
Feature Store
// ✅ GOOD - NgRx feature with createFeature export const usersFeature = createFeature({ name: 'users', reducer: createReducer( initialState, on(UsersActions.loadUsers, state => ({ ...state, loading: true })), on(UsersActions.loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false, })), on(UsersActions.loadUsersFailure, (state, { error }) => ({ ...state, error, loading: false, })), ), });
export const { selectUsers, selectLoading, selectError, } = usersFeature;
Actions
// ✅ GOOD - createActionGroup export const UsersActions = createActionGroup({ source: 'Users', events: { 'Load Users': emptyProps(), 'Load Users Success': props<{ users: User[] }>(), 'Load Users Failure': props<{ error: string }>(), 'Select User': props<{ userId: string }>(), }, });
Effects
// ✅ GOOD - Functional effects export const loadUsers = createEffect( (actions$ = inject(Actions), userService = inject(UserService)) => { return actions$.pipe( ofType(UsersActions.loadUsers), exhaustMap(() => userService.getUsers().pipe( map(users => UsersActions.loadUsersSuccess({ users })), catchError(error => of(UsersActions.loadUsersFailure({ error: error.message })) ), ), ), ); }, { functional: true }, );
- Forms
Reactive Forms
// ✅ GOOD - Typed reactive forms @Component({...}) export class UserFormComponent { private fb = inject(NonNullableFormBuilder);
form = this.fb.group({ email: ['', [Validators.required, Validators.email]], name: ['', [Validators.required, Validators.minLength(2)]], password: ['', [Validators.required, Validators.minLength(8)]], });
onSubmit() { if (this.form.valid) { const value = this.form.getRawValue(); // value is typed: { email: string; name: string; password: string } this.save(value); } } }
Template
<form [formGroup]="form" (ngSubmit)="onSubmit()"> <div> <label for="email">Email</label> <input id="email" formControlName="email" type="email"> @if (form.controls.email.errors?.['required']) { <span class="error">Email is required</span> } @if (form.controls.email.errors?.['email']) { <span class="error">Invalid email format</span> } </div>
<button type="submit" [disabled]="form.invalid">Submit</button> </form>
- Routing
Lazy Loading
// ✅ GOOD - Lazy loaded routes export const routes: Routes = [ { path: 'users', loadComponent: () => import('./users/users.component').then(m => m.UsersComponent), children: [ { path: ':id', loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent), }, ], }, ];
Route Guards
// ✅ GOOD - Functional guards export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router);
if (authService.isAuthenticated()) { return true; }
return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }, }); };
Resolvers
// ✅ GOOD - Functional resolver export const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); const userId = route.paramMap.get('id')!; return userService.getUser(userId); };
// Usage in routes { path: ':id', component: UserDetailComponent, resolve: { user: userResolver }, }
- Performance Optimization
OnPush Change Detection
// ✅ GOOD - Always use OnPush @Component({ changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyComponent {}
trackBy for ngFor
// ✅ GOOD - trackBy for lists
@Component({
template: @for (user of users; track user.id) { <app-user-card [user]="user" /> } ,
})
export class UsersComponent {
users: User[] = [];
}
Defer Loading
// ✅ GOOD - Defer heavy components
@Component({
template: @defer (on viewport) { <app-heavy-component /> } @placeholder { <div class="skeleton"></div> } @loading { <div class="spinner"></div> } ,
})
export class MyComponent {}
- HTTP Interceptors
// ✅ GOOD - Functional interceptor export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.getToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: Bearer ${token} },
});
}
return next(req); };
// In app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withInterceptors([authInterceptor])), ], };
- Testing
// ✅ GOOD - Component testing describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent], }).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should display user name', () => { component.user = { id: '1', name: 'John', email: 'john@example.com' }; fixture.detectChanges();
const nameElement = fixture.nativeElement.querySelector('h3');
expect(nameElement.textContent).toContain('John');
});
it('should emit when clicked', () => { component.user = { id: '1', name: 'John', email: 'john@example.com' }; jest.spyOn(component.selected, 'emit');
fixture.nativeElement.querySelector('.user-card').click();
expect(component.selected.emit).toHaveBeenCalledWith(component.user);
}); });
Quick Reference
checklist[12]{pattern,best_practice}: Components,Standalone + OnPush + Signals State,Signals for local NgRx for global Forms,NonNullableFormBuilder typed RxJS,takeUntilDestroyed + async pipe Routes,Lazy loading + functional guards DI,inject() function Lists,@for with track Defer,@defer for heavy components HTTP,Functional interceptors Testing,ComponentFixture + TestBed Errors,catchError with recovery Smart/Dumb,Container vs presentational
Version: 1.3.0