Angular Signals và Computed năm 2026: Reactivity Chi Tiết và Câu Hỏi Phỏng Vấn
Tìm hiểu Angular Signals và computed signals để có reactivity chi tiết trong Angular 20+. Hướng dẫn toàn diện bao gồm signal primitives, linkedSignal, tương tác RxJS và các câu hỏi phỏng vấn kỹ thuật.

Angular Signals đại diện cho sự thay đổi quan trọng nhất trong mô hình reactivity của Angular kể từ khi framework này ra đời. Được giới thiệu trong Angular 17 và hiện là phương pháp mặc định trong Angular 20+, signals cung cấp reactivity chi tiết loại bỏ các chu kỳ change detection không cần thiết và tích hợp liền mạch với các ứng dụng zoneless.
Signals là các primitive reactive đồng bộ và không có glitch. Computed signal chỉ tính toán lại khi dependencies thay đổi và chỉ một lần mỗi chu kỳ thay đổi, bất kể signal được đọc bao nhiêu lần.
Hiểu Signal Primitives trong Angular 20
Hệ thống signal của Angular bao gồm ba primitive cốt lõi: signal(), computed(), và effect(). Mỗi primitive phục vụ một mục đích riêng biệt trong reactive graph.
Writable signal lưu trữ giá trị có thể được cập nhật bằng .set() hoặc .update(). Đọc signal trả về giá trị hiện tại, và bất kỳ computed signal hoặc effect nào phụ thuộc vào nó sẽ được thông báo về các thay đổi.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal với giá trị khởi tạo
count = signal(0);
// Computed signal được dẫn xuất từ count
// Chỉ tính toán lại khi count thay đổi
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() nhận giá trị hiện tại làm tham số
this.count.update(c => c + 1);
}
}Sự khác biệt chính so với RxJS BehaviorSubject: signals là đồng bộ và không có glitch. Khi count thay đổi, doubleCount tính toán lại đúng một lần, ngay cả khi được đọc nhiều lần trong cùng một block đồng bộ.
Computed Signals: Đánh Giá Lazy và Memoization
Computed signals cache kết quả cho đến khi dependency thay đổi. Memoization này là tự động, không giống RxJS nơi shareReplay hoặc distinctUntilChanged phải được thêm thủ công.
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed chỉ theo dõi các properties được đọc
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Phụ thuộc vào computed signal khác
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Computed signal đánh giá theo kiểu lazy: hàm computation chỉ chạy khi signal được đọc lần đầu, không phải khi khai báo. Các lần đọc tiếp theo trả về giá trị được cache cho đến khi dependency làm mất hiệu lực.
Effect: Side Effects trong Reactive Graph
Effects thực thi code để phản hồi các thay đổi signal. Chúng chạy bất đồng bộ trong chu kỳ change detection, sau khi tất cả các cập nhật đồng bộ hoàn thành.
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect đồng bộ state signal với thuộc tính DOM
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect cho persistence localStorage
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects theo dõi dependencies một cách động. Nếu một nhánh điều kiện không được thực thi, các signals chỉ được đọc trong nhánh đó không được theo dõi cho đến khi điều kiện thay đổi.
Sử dụng effect để sao chép dữ liệu từ một signal sang signal khác cho thấy vấn đề thiết kế. Nên sử dụng computed cho derived state hoặc linkedSignal khi giá trị dẫn xuất cần có thể ghi được.
linkedSignal: Writable Derived State
Được giới thiệu trong Angular 19 và ổn định trong Angular 20, linkedSignal tạo writable signal được reset khi nguồn thay đổi. Nó giải quyết các tình huống khi computed là read-only nhưng giá trị dẫn xuất cần sửa đổi cục bộ.
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal reset về 1 khi pageSize hoặc totalItems thay đổi
// nhưng cho phép điều hướng thủ công qua prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}Không có linkedSignal, pattern này yêu cầu effect ghi vào signal khác (anti-pattern) hoặc điều phối RxJS phức tạp.
Sẵn sàng chinh phục phỏng vấn Angular?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Signal-Based Component APIs: input, model, viewChild
Angular 20 đưa signal-based component APIs lên trạng thái ổn định. Chúng thay thế decorators bằng các alternative reactive tích hợp vào signal graph.
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input thay thế @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() tạo signal có thể bind hai chiều
// Parent sử dụng [(selected)]="parentSignal"
selected = model(false);
// Output emitter cho events
cardClicked = output<void>();
// viewChild trả về Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed dẫn xuất từ signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputs cung cấp type safety tốt hơn: input.required() trả về InputSignal<T> trong khi input() trả về InputSignal<T | undefined> trừ khi có default được cung cấp.
Signals vs RxJS: Hiệu Năng Dưới Tải Cao
Benchmarks trong Angular 21+ cho thấy signals giảm overhead change detection trong các tình huống UI-heavy. Dưới tải cao với cập nhật thường xuyên, signals cho thấy memory profiles sạch hơn và sử dụng CPU thấp hơn so với bindings dựa trên RxJS.
Sự phân tách concerns rõ ràng:
| Use Case | Phương Pháp Được Khuyến Nghị |
|---|---|
| State component cục bộ | signal() |
| Giá trị UI dẫn xuất | computed() |
| Đồng bộ với APIs bên ngoài | effect() |
| HTTP requests | RxJS Observable + toSignal() |
| WebSocket streams | RxJS với operators |
| Input người dùng có debounce | RxJS debounceTime, sau đó toSignal() |
| Điều phối async phức tạp | RxJS pipelines |
RxJS vẫn là công cụ cho async streams, cancellation, và backpressure. Signals xử lý state đồng bộ.
Tương Tác RxJS: toSignal và toObservable
Angular cung cấp các utilities trong @angular/core/rxjs-interop để kết nối signals và observables.
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Chuyển đổi signal thành observable cho operators RxJS
private query$ = toObservable(this.query);
// RxJS pipeline với debounce, sau đó quay lại signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignal subscribe khi được tạo và unsubscribe khi component bị destroy. Tùy chọn initialValue loại bỏ type undefined khỏi signal.
Câu Hỏi Phỏng Vấn: Angular Signals
Các cuộc phỏng vấn kỹ thuật năm 2026 thường xuyên kiểm tra sự hiểu biết về signal. Dưới đây là các patterns phân biệt ứng viên senior.
Q: Điều gì xảy ra nếu computed signal throw error?
Error lan truyền đến bất kỳ effect hoặc computed nào đọc nó. Angular không cache errors, computation thử lại ở lần đọc tiếp theo. Code production nên xử lý errors bên trong hàm computed.
Q: Có thể ghi vào signal bên trong computed không?
Không. Cố gắng gọi .set() hoặc .update() bên trong computed sẽ throw error. Các hàm computed phải pure. Sử dụng linkedSignal nếu cần giá trị dẫn xuất cũng có thể ghi được.
Q: Effects xử lý đọc signal trong điều kiện như thế nào?
// Ví dụ theo dõi dependency động
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails chỉ được theo dõi khi showDetails là true
console.log('Details:', userDetails().email);
}
});Effects theo dõi dependencies dựa trên lần thực thi gần nhất. Nếu showDetails là false, các thay đổi với userDetails không kích hoạt effect.
Q: Equality của signal hoạt động như thế nào?
Mặc định, signals sử dụng Object.is cho equality. Với objects, điều này có nghĩa là reference equality. Custom equality có thể được cung cấp:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Ứng viên đề cập rằng computed signals được memoize và đánh giá lazy cho thấy sự hiểu biết vượt ra ngoài việc sử dụng API cơ bản. Thảo luận khi nào sử dụng linkedSignal vs computed cho thấy sự quen thuộc với các patterns Angular 19+.
Change Detection: Signals và Zoneless Angular
Angular 22 mặc định sử dụng zoneless change detection cho các dự án mới. Signals là trung tâm của điều này: chúng thông báo cho Angular chính xác khi state thay đổi, loại bỏ nhu cầu Zone.js patch các async APIs.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Cập nhật signal kích hoạt change detection
// Không cần Zone.js
this.count.update(c => c + 1);
}
}Với signals, Angular theo dõi chính xác components nào phụ thuộc vào state nào. Cập nhật signal lên lịch change detection chỉ cho các components bị ảnh hưởng, không phải toàn bộ tree.
Resource API: Async Data dưới dạng Signals
Resource API của Angular 20 (resource() và rxResource()) tải async data vào signals một cách declarative, thay thế các patterns subscription thủ công.
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Resource async declarative
// Refetch khi userId thay đổi
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Resource tự động theo dõi loading state, errors, và giá trị đã resolve. Nó refetch khi request signal thay đổi.
Best Practices cho Kiến Trúc Signal
Cấu trúc signals trong các ứng dụng lớn hơn đòi hỏi kỷ luật. Các patterns này scale từ components đến services.
Colocate các signals liên quan: Nhóm các signals thay đổi cùng nhau. Component form giữ các field signals của nó cùng nhau, không phân tán qua các services.
Nâng shared state lên services: Khi nhiều components cần cùng state, di chuyển signal đến injectable service:
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal cho consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}Ưu tiên computed hơn effect: Nếu giá trị có thể dẫn xuất được, sử dụng computed. Effects dành cho side effects rời khỏi reactive graph: thao tác DOM, localStorage, analytics, network requests.
Di Chuyển từ RxJS sang Signals
Các codebase Angular legacy có thể di chuyển dần dần. Các utilities interop RxJS cho phép signals và observables cùng tồn tại.
// Trước: Dựa trên RxJS
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// Sau: Dựa trên Signal
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Với các operations async, giữ RxJS cho pipeline và chuyển đổi ở boundary:
// Phương pháp hybrid
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Chuẩn Bị Câu Hỏi Phỏng Vấn Angular Signals
- Signals là các primitives đồng bộ; RxJS xử lý async streams
- Computed signals là lazy và được memoize, tính toán lại chỉ khi dependencies thay đổi
- Effects chạy bất đồng bộ trong change detection, không phải ngay lập tức
- linkedSignal giải quyết vấn đề writable-derived-state mà không cần effects
- Signal inputs (
input(),model()) thay thế decorators bằng các alternative reactive toSignalvàtoObservablekết nối signals và RxJS để di chuyển dần dần- Zoneless Angular dựa vào signals cho change detection chi tiết
- Resource API (
rxResource) quản lý async state một cách declarative - Hướng dẫn chính thức Angular signals vẫn là tài liệu tham khảo có thẩm quyền cho chi tiết API và best practices
Bạn có tìm ra lỗi trong Angular không?
Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Viết bởi
Anthony Fillion-MailletNgười sáng lập SharpSkill
Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.
Cập nhật ngày 17 tháng 9, 2026
Thẻ
Chia sẻ
Bài viết liên quan

Câu hỏi phỏng vấn Angular 19: Signals, SSR và những khái niệm bắt buộc phải biết
Những câu hỏi phỏng vấn Angular 19 phổ biến nhất: Signals, hydration tăng dần, zoneless change detection và các API reactive mới kèm ví dụ code và câu trả lời mong đợi.

Dependency Injection Nang Cao trong Angular 2026: Providers, Tokens va Cau Hoi Phong Van
Tim hieu sau ve he thong dependency injection cua Angular bao gom chien luoc provider, InjectionToken, injector phan cap va cac cau hoi phong van thuong gap cho developer Angular co kinh nghiem.

Cú Pháp Control Flow trong Angular 2026: @if, @for, @switch và Câu Hỏi Phỏng Vấn
Tìm hiểu cú pháp control flow Angular (@if, @for, @switch) thay thế các structural directive. Hướng dẫn đầy đủ với ví dụ code, tips migration và câu hỏi phỏng vấn kỹ thuật.