Claude-skill-registry frontend-web-dev
Use when building Angular applications, creating TypeScript components, implementing reactive forms, managing state, working with RxJS observables, or developing modern web UI with TypeScript.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-web-dev" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-web-dev && rm -rf "$T"
skills/data/frontend-web-dev/SKILL.mdFrontend Web Development Expert
Overview
Expert guidance for building modern web applications with Angular 20+, TypeScript, Signals, and RxJS. Focused on standalone components, signal-based reactivity, and type-safe development.
When to Use
- Creating Angular components, services, or standalone features
- Implementing reactive forms or template-driven forms
- Working with Signals for state management
- Working with RxJS observables for async streams
- Implementing HTTP clients and API integration
- Creating reusable UI components
- Implementing routing and navigation
- Working with Angular dependency injection
Angular 20+ Core Patterns
Standalone Component with Signals (Preferred)
// Good: Modern Angular 20 component with signals and OnPush import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core'; interface User { readonly id: number; readonly name: string; readonly email: string; } @Component({ selector: 'app-user-card', standalone: true, imports: [CommonModule], templateUrl: './user-card.component.html', styleUrl: './user-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) export class UserCardComponent { // Signal-based inputs (Angular 17+) user = input.required<User>(); // Signal-based outputs userSelected = output<User>(); // Computed signals for derived state displayName = computed(() => this.user().name.toUpperCase()); onSelect(): void { this.userSelected.emit(this.user()); } }
// Bad: Outdated patterns @Component({ selector: 'user-card', template: `<div>{{user.name}}</div>` // Missing: standalone, OnPush, proper typing }) export class UserCard { user: any; // No type safety! @Output() selected: any; // Old decorator syntax + any type }
Service with Signals for State
// Good: Modern service using signals for state management import { Injectable, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; interface User { readonly id: number; readonly name: string; readonly email: string; readonly active: boolean; } @Injectable({ providedIn: 'root' }) export class UserService { private readonly apiUrl = '/api/users'; // Signal-based state private readonly usersState = signal<User[]>([]); private readonly loadingState = signal(false); private readonly errorState = signal<string | null>(null); // Public readonly computed signals readonly users = this.usersState.asReadonly(); readonly loading = this.loadingState.asReadonly(); readonly error = this.errorState.asReadonly(); // Computed derived state readonly activeUsers = computed(() => this.usersState().filter(u => u.active) ); readonly userCount = computed(() => this.usersState().length); constructor(private http: HttpClient) {} loadUsers(): void { this.loadingState.set(true); this.errorState.set(null); this.http.get<User[]>(this.apiUrl).subscribe({ next: (users) => { this.usersState.set(users); this.loadingState.set(false); }, error: (err) => { this.errorState.set(err.message); this.loadingState.set(false); } }); } addUser(user: User): void { // Immutable update this.usersState.update(users => [...users, user]); } }
Reactive Forms with Typed FormGroup
// Good: Strongly-typed reactive forms (Angular 14+) import { Component, ChangeDetectionStrategy } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; interface UserFormValue { email: string; name: string; age: number | null; } @Component({ selector: 'app-user-form', standalone: true, imports: [ReactiveFormsModule], templateUrl: './user-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class UserFormComponent { // Typed form group userForm = this.fb.nonNullable.group({ email: ['', [Validators.required, Validators.email]], name: ['', [Validators.required, Validators.minLength(2)]], age: [null as number | null, [Validators.min(0), Validators.max(150)]] }); constructor(private fb: FormBuilder) {} onSubmit(): void { if (this.userForm.valid) { const userData: UserFormValue = this.userForm.getRawValue(); // Process typed form data } } }
Quick Reference: Angular 20+ Best Practices
| Pattern | Recommendation |
|---|---|
| Components | Standalone with |
| Change Detection | Always use for performance |
| State (local) | Signals: , |
| State (shared) | Services with signals |
| Async streams | RxJS for HTTP, events, WebSockets |
| Inputs | / (signal-based) |
| Outputs | (signal-based) |
| Forms | Typed reactive forms with |
| Type Safety | Strict TypeScript, interfaces for all data |
Change Detection: OnPush Explained
Angular checks components for changes to update the DOM. There are two strategies:
Default: Check this component on EVERY change detection cycle (expensive)
OnPush: Only check this component when:
- An
or@Input()
reference changesinput() - An event originates from this component or its children
- A signal used in the template updates
- Manually triggered via
ChangeDetectorRef
// ALWAYS use OnPush - it's a free performance win @Component({ // ... changeDetection: ChangeDetectionStrategy.OnPush })
With signals, OnPush becomes even more efficient - Angular knows exactly which signal changed and only updates the affected DOM nodes.
Signals vs RxJS: When to Use Each
| Use Case | Use Signals | Use RxJS |
|---|---|---|
| Component state | ✅ | ❌ |
| Derived/computed values | ✅ | ❌ |
| Service state | ✅ | ⚠️ BehaviorSubject (legacy) |
| HTTP requests | ❌ | ✅ returns Observable |
| Event streams | ❌ | ✅ Multiple values over time |
| Debounce/throttle | ❌ | ✅ RxJS operators |
| Combining async sources | ⚠️ Limited | ✅ , |
// Signals: Synchronous state const count = signal(0); const doubled = computed(() => count() * 2); // RxJS: Async streams (HTTP, events, WebSockets) this.http.get<User[]>('/api/users').pipe( catchError(err => of([])) ).subscribe(users => this.usersState.set(users));
Common Mistakes
Forgetting OnPush:
// Bad: Missing OnPush (checks every cycle) @Component({ selector: 'app-foo', ... }) // Good: Always include OnPush @Component({ selector: 'app-foo', changeDetection: ChangeDetectionStrategy.OnPush, ... })
Mutating state directly:
// Bad: Direct mutation (won't trigger change detection with OnPush) this.users.push(newUser); this.usersSignal().push(newUser); // Also bad! // Good: Immutable update this.users = [...this.users, newUser]; this.usersSignal.update(users => [...users, newUser]);
Not typing data structures:
// Bad: Lose type safety const data: any = response; navItems = signal([{ label: 'Home', icon: 'home' }]); // Inline, no interface // Good: Define interfaces interface NavItem { readonly label: string; readonly icon: string; readonly route: string; } const navItems = signal<NavItem[]>([...]);
Memory leaks with RxJS (still relevant for HTTP/events):
// Bad: Subscription leak ngOnInit() { this.service.getData().subscribe(data => { this.data = data; }); } // Good: Use takeUntilDestroyed (Angular 16+) import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class MyComponent { private destroyRef = inject(DestroyRef); ngOnInit() { this.service.getData() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(data => this.dataSignal.set(data)); } } // Better: Convert to signal with toSignal() import { toSignal } from '@angular/core/rxjs-interop'; export class MyComponent { data = toSignal(this.service.getData(), { initialValue: [] }); }
TypeScript Best Practices
// Good: Strict interfaces with readonly properties interface User { readonly id: number; readonly email: string; readonly name: string; readonly createdAt: Date; } // Good: Utility types for variations type UserCreate = Omit<User, 'id' | 'createdAt'>; type UserUpdate = Partial<UserCreate>; // Good: Discriminated unions for state type LoadingState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string };
Testing Patterns
// Good: Testing standalone component with signals import { ComponentFixture, TestBed } from '@angular/core/testing'; describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture<UserCardComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent] // Standalone: import the component itself }).compileComponents(); fixture = TestBed.createComponent(UserCardComponent); component = fixture.componentInstance; }); it('should emit userSelected when clicked', () => { const user: User = { id: 1, name: 'Test', email: 'test@example.com' }; // Set signal input using componentRef fixture.componentRef.setInput('user', user); fixture.detectChanges(); let emittedUser: User | undefined; component.userSelected.subscribe(u => emittedUser = u); component.onSelect(); expect(emittedUser).toEqual(user); }); });
Key Principles
- Standalone components: Always use
standalone: true - OnPush always: Free performance optimization
- Signals for state: Simpler than RxJS for synchronous state
- RxJS for streams: HTTP calls, events, WebSockets
- Type everything: Interfaces for all data structures
- Immutable updates: Never mutate, always create new references
- Modern inputs/outputs: Use
andinput()
functionsoutput()