| name | angular-frontend-patterns |
| description | Modern Angular development patterns with signals and reactive architecture. Use when (1) Building Angular components with signals, (2) Implementing smart/dumb component patterns, (3) State management with signals, (4) RxJS integration patterns, (5) Push-based architecture, (6) Component communication, (7) Angular routing patterns, (8) Form handling with signals. |
Modern Angular Frontend Patterns
Signal-first, push-based architecture patterns for Angular 17+.
Core Principle: Signals Over Observables
Use signals for synchronous state, observables for async streams.
| Use Case | Pattern |
|---|
| Component state | signal(), computed() |
| API responses | toSignal() from observable |
| Form values | signal() for model |
| Event streams | Keep as Observable |
| WebSocket streams | Keep as Observable |
Signal Types Quick Reference
import { signal, computed, effect, input, output, model } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
const count = signal(0);
count.set(5);
count.update((v) => v + 1);
const doubled = computed(() => count() * 2);
effect(() => console.log(count()));
name = input<string>();
userId = input.required<string>();
size = input(10);
active = input(false, { transform: booleanAttribute });
clicked = output<MouseEvent>();
value = model(0);
Smart vs Dumb Components
Smart (Container) Components
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent, UserFilterComponent],
template: `
<app-user-filter (filterChange)="onFilterChange($event)" />
@if (loading()) {
<app-spinner />
} @else { @for (user of filteredUsers(); track user.id) {
<app-user-card [user]="user" (select)="onSelectUser($event)" />
} }
`,
})
export class UserListComponent {
private userService = inject(UserService);
private router = inject(Router);
private filter = signal('');
loading = signal(false);
users = toSignal(this.userService.users$, { initialValue: [] });
filteredUsers = computed(() => {
const filter = this.filter().toLowerCase();
return this.users().filter((u) => u.name.toLowerCase().includes(filter));
});
onFilterChange(value: string) {
this.filter.set(value);
}
onSelectUser(userId: string) {
this.router.navigate(['/users', userId]);
}
}
Dumb (Presentational) Components
@Component({
selector: 'app-user-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card" (click)="select.emit(user().id)">
<img [src]="user().avatar" [alt]="user().name" />
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
</div>
`,
})
export class UserCardComponent {
user = input.required<User>();
select = output<string>();
}
State Management Pattern
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private _users = signal<User[]>([]);
private _selectedUserId = signal<string | null>(null);
private _loading = signal(false);
private _error = signal<string | null>(null);
users = this._users.asReadonly();
loading = this._loading.asReadonly();
error = this._error.asReadonly();
selectedUser = computed(() => {
const id = this._selectedUserId();
return this._users().find((u) => u.id === id) ?? null;
});
activeUsers = computed(() => this._users().filter((u) => u.isActive));
users$ = this.http.get<User[]>('/api/users');
loadUsers() {
this._loading.set(true);
this._error.set(null);
this.http
.get<User[]>('/api/users')
.pipe(
tap((users) => {
this._users.set(users);
this._loading.set(false);
}),
catchError((err) => {
this._error.set(err.message);
this._loading.set(false);
return EMPTY;
})
)
.subscribe();
}
selectUser(id: string | null) {
this._selectedUserId.set(id);
}
addUser(user: User) {
this._users.update((users) => [...users, user]);
}
updateUser(id: string, changes: Partial<User>) {
this._users.update((users) => users.map((u) => (u.id === id ? { ...u, ...changes } : u)));
}
}
RxJS Integration
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
const users = toSignal(this.http.get<User[]>('/api/users'), {
initialValue: [],
});
const usersResource = toSignal(
this.http.get<User[]>('/api/users').pipe(
map((data) => ({ data, loading: false, error: null })),
startWith({ data: [], loading: true, error: null }),
catchError((err) => of({ data: [], loading: false, error: err.message }))
)
);
const searchTerm = signal('');
const searchTerm$ = toObservable(this.searchTerm);
const searchResults = toSignal(
toObservable(this.searchTerm).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((term) => this.searchService.search(term))
),
{ initialValue: [] }
);
Decision Matrix
References
Load these for detailed implementation guidance: