Angular Signals และ Computed ในปี 2026: Fine-Grained Reactivity และคำถามสัมภาษณ์
เรียนรู้ Angular Signals และ computed signals สำหรับ fine-grained reactivity ใน Angular 20+ คู่มือครบถ้วนครอบคลุม signal primitives, linkedSignal, การทำงานร่วมกับ RxJS และคำถามสัมภาษณ์ทางเทคนิค

Angular Signals เป็นการเปลี่ยนแปลงที่สำคัญที่สุดใน reactivity model ของ Angular นับตั้งแต่ framework นี้ถูกสร้างขึ้น ถูกแนะนำใน Angular 17 และตอนนี้เป็นแนวทางเริ่มต้นใน Angular 20+ signals ให้ fine-grained reactivity ที่ขจัดรอบ change detection ที่ไม่จำเป็นและรวมเข้ากับแอปพลิเคชัน zoneless ได้อย่างราบรื่น
Signals เป็น reactive primitives ที่ synchronous และไม่มี glitch Computed signal จะคำนวณใหม่เฉพาะเมื่อ dependencies เปลี่ยนแปลงและคำนวณเพียงครั้งเดียวต่อรอบการเปลี่ยนแปลง ไม่ว่า signal จะถูกอ่านกี่ครั้งก็ตาม
ทำความเข้าใจ Signal Primitives ใน Angular 20
ระบบ signal ของ Angular ประกอบด้วย primitives หลักสามตัว: signal(), computed() และ effect() แต่ละตัวมีวัตถุประสงค์ที่แตกต่างกันใน reactive graph
Writable signal เก็บค่าที่สามารถอัปเดตด้วย .set() หรือ .update() การอ่าน signal จะคืนค่าปัจจุบัน และ computed signals หรือ effects ที่ขึ้นอยู่กับมันจะได้รับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลง
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 พร้อมค่าเริ่มต้น
count = signal(0);
// Computed signal ถูกสร้างจาก count
// คำนวณใหม่เฉพาะเมื่อ count เปลี่ยน
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() รับค่าปัจจุบันเป็น argument
this.count.update(c => c + 1);
}
}ความแตกต่างหลักจาก RxJS BehaviorSubject: signals เป็น synchronous และไม่มี glitch เมื่อ count เปลี่ยน doubleCount จะคำนวณใหม่เพียงครั้งเดียว แม้จะถูกอ่านหลายครั้งใน synchronous block เดียวกัน
Computed Signals: การประเมินแบบ Lazy และ Memoization
Computed signals แคชผลลัพธ์จนกว่า dependency จะเปลี่ยน Memoization นี้เป็นไปโดยอัตโนมัติ ไม่เหมือน RxJS ที่ต้องเพิ่ม shareReplay หรือ distinctUntilChanged ด้วยตนเอง
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 ติดตามเฉพาะ properties ที่ถูกอ่าน
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// ขึ้นอยู่กับ computed signal อื่น
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Computed signal ประเมินแบบ lazy: ฟังก์ชัน computation ทำงานเฉพาะเมื่อ signal ถูกอ่านครั้งแรก ไม่ใช่เมื่อประกาศ การอ่านครั้งต่อไปจะคืนค่าที่ถูกแคชจนกว่า dependency จะทำให้มันไม่ถูกต้อง
Effect: Side Effects ใน Reactive Graph
Effects ทำงานเป็นการตอบสนองต่อการเปลี่ยนแปลงของ signal โดยทำงานแบบ asynchronous ระหว่างรอบ change detection หลังจากการอัปเดต synchronous ทั้งหมดเสร็จสิ้น
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 ซิงค์ state ของ signal กับ attribute ของ DOM
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect สำหรับ persistence ใน localStorage
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects ติดตาม dependencies แบบไดนามิก หากสาขาเงื่อนไขไม่ถูกดำเนินการ signals ที่อ่านเฉพาะในสาขานั้นจะไม่ถูกติดตามจนกว่าเงื่อนไขจะเปลี่ยน
การใช้ effect เพื่อคัดลอกข้อมูลจาก signal หนึ่งไปยังอีก signal บ่งชี้ปัญหาในการออกแบบ ควรใช้ computed สำหรับ derived state หรือ linkedSignal เมื่อค่าที่สร้างขึ้นต้องสามารถเขียนได้
linkedSignal: Writable Derived State
ถูกแนะนำใน Angular 19 และเสถียรใน Angular 20 linkedSignal สร้าง writable signal ที่รีเซ็ตเมื่อแหล่งที่มาเปลี่ยน มันแก้ไขสถานการณ์ที่ computed เป็น read-only แต่ค่าที่สร้างขึ้นต้องการการแก้ไขในท้องถิ่น
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 รีเซ็ตเป็น 1 เมื่อ pageSize หรือ totalItems เปลี่ยน
// แต่อนุญาตให้นำทางด้วยตนเองผ่าน 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));
}
}หากไม่มี linkedSignal รูปแบบนี้จะต้องใช้ effect ที่เขียนไปยัง signal อื่น (anti-pattern) หรือการประสานงาน RxJS ที่ซับซ้อน
พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Signal-Based Component APIs: input, model, viewChild
Angular 20 ยกระดับ signal-based component APIs ให้เป็นสถานะเสถียร สิ่งเหล่านี้แทนที่ decorators ด้วยทางเลือก reactive ที่รวมเข้ากับ 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 แทนที่ @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() สร้าง signal ที่สามารถ bind สองทาง
// Parent ใช้ [(selected)]="parentSignal"
selected = model(false);
// Output emitter สำหรับ events
cardClicked = output<void>();
// viewChild คืน Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed สร้างจาก signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputs ให้ type safety ที่ดีกว่า: input.required() คืน InputSignal<T> ในขณะที่ input() คืน InputSignal<T | undefined> เว้นแต่จะให้ default
Signals vs RxJS: ประสิทธิภาพภายใต้โหลดสูง
Benchmarks ใน Angular 21+ แสดงให้เห็นว่า signals ลด overhead ของ change detection ในสถานการณ์ UI-heavy ภายใต้โหลดสูงที่มีการอัปเดตบ่อย signals แสดง memory profiles ที่สะอาดกว่า และใช้ CPU ต่ำกว่าเมื่อเทียบกับ bindings ที่ใช้ RxJS
การแยก concerns ชัดเจน:
| Use Case | แนวทางที่แนะนำ |
|---|---|
| State ของ component ในเครื่อง | signal() |
| ค่า UI ที่สร้างขึ้น | computed() |
| ซิงค์กับ APIs ภายนอก | effect() |
| HTTP requests | RxJS Observable + toSignal() |
| WebSocket streams | RxJS พร้อม operators |
| Input ของผู้ใช้ที่มี debounce | RxJS debounceTime แล้ว toSignal() |
| การประสานงาน async ที่ซับซ้อน | RxJS pipelines |
RxJS ยังคงเป็นเครื่องมือสำหรับ async streams, cancellation และ backpressure Signals จัดการ state แบบ synchronous
การทำงานร่วมกับ RxJS: toSignal และ toObservable
Angular มี utilities ใน @angular/core/rxjs-interop สำหรับเชื่อมต่อ signals และ 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('');
// แปลง signal เป็น observable สำหรับ operators ของ RxJS
private query$ = toObservable(this.query);
// RxJS pipeline พร้อม debounce แล้วกลับเป็น 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 เมื่อถูกสร้างและ unsubscribe เมื่อ component ถูกทำลาย ตัวเลือก initialValue ขจัด type undefined ออกจาก signal
คำถามสัมภาษณ์: Angular Signals
การสัมภาษณ์ทางเทคนิคในปี 2026 มักทดสอบความเข้าใจเกี่ยวกับ signal ต่อไปนี้คือรูปแบบที่แยกแยะผู้สมัครระดับ senior
Q: จะเกิดอะไรขึ้นถ้า computed signal throw error?
Error จะแพร่กระจายไปยัง effect หรือ computed ใดก็ตามที่อ่านมัน Angular ไม่แคช errors การ computation ลองใหม่ในการอ่านครั้งต่อไป โค้ด production ควรจัดการ errors ภายในฟังก์ชัน computed
Q: สามารถเขียนไปยัง signal ภายใน computed ได้หรือไม่?
ไม่ การพยายามเรียก .set() หรือ .update() ภายใน computed จะ throw error ฟังก์ชัน computed ต้องเป็น pure ใช้ linkedSignal หากต้องการค่าที่สร้างขึ้นที่สามารถเขียนได้ด้วย
Q: Effects จัดการการอ่าน signal ในเงื่อนไขอย่างไร?
// ตัวอย่างการติดตาม dependency แบบไดนามิก
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails ถูกติดตามเฉพาะเมื่อ showDetails เป็น true
console.log('Details:', userDetails().email);
}
});Effects ติดตาม dependencies ตามการทำงานล่าสุด หาก showDetails เป็น false การเปลี่ยนแปลง userDetails จะไม่ trigger effect
Q: equality ของ signal ทำงานอย่างไร?
โดยค่าเริ่มต้น signals ใช้ Object.is สำหรับ equality สำหรับ objects นี่หมายถึง reference equality สามารถให้ custom equality ได้:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);ผู้สมัครที่กล่าวถึงว่า computed signals ถูก memoize และประเมินแบบ lazy แสดงให้เห็นความเข้าใจที่เกินกว่าการใช้ API พื้นฐาน การอภิปรายว่าเมื่อใดควรใช้ linkedSignal vs computed แสดงให้เห็นความคุ้นเคยกับรูปแบบ Angular 19+
Change Detection: Signals และ Zoneless Angular
Angular 22 ใช้ zoneless change detection เป็นค่าเริ่มต้นสำหรับโปรเจกต์ใหม่ Signals เป็นศูนย์กลางของสิ่งนี้: พวกมันแจ้ง Angular อย่างแม่นยำว่า state เปลี่ยนเมื่อใด ขจัดความจำเป็นที่ Zone.js จะต้อง patch 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 {
// การอัปเดต signal trigger change detection
// ไม่ต้องการ Zone.js
this.count.update(c => c + 1);
}
}ด้วย signals Angular ติดตามอย่างแม่นยำว่า components ใดขึ้นอยู่กับ state ใด การอัปเดต signal กำหนดเวลา change detection เฉพาะสำหรับ components ที่ได้รับผลกระทบ ไม่ใช่ทั้ง tree
Resource API: Async Data เป็น Signals
Resource API ของ Angular 20 (resource() และ rxResource()) โหลด async data เข้าสู่ signals แบบ declarative แทนที่รูปแบบ subscription แบบ manual
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 เมื่อ userId เปลี่ยน
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Resource ติดตาม loading state, errors และค่าที่ resolve โดยอัตโนมัติ มันจะ refetch เมื่อ request signal เปลี่ยน
Best Practices สำหรับสถาปัตยกรรม Signal
การจัดโครงสร้าง signals ในแอปพลิเคชันขนาดใหญ่ต้องการวินัย รูปแบบเหล่านี้ scale จาก components ถึง services
Colocate signals ที่เกี่ยวข้อง: จัดกลุ่ม signals ที่เปลี่ยนแปลงพร้อมกัน Form component เก็บ field signals ไว้ด้วยกัน ไม่กระจายไปทั่ว services
ยก shared state ขึ้นไปยัง services: เมื่อหลาย components ต้องการ state เดียวกัน ย้าย signal ไปยัง 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 ให้ 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 }];
});
}
}ใช้ computed มากกว่า effect: หากค่าสามารถสร้างได้ ให้ใช้ computed Effects มีไว้สำหรับ side effects ที่ออกจาก reactive graph: การจัดการ DOM, localStorage, analytics, network requests
การย้ายจาก RxJS ไปยัง Signals
Codebases Angular เก่าสามารถย้ายได้ทีละขั้นตอน Utilities interop ของ RxJS อนุญาตให้ signals และ observables อยู่ร่วมกัน
// ก่อน: ใช้ 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')
);
}
// หลัง: ใช้ Signal
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}สำหรับ operations แบบ async ให้ใช้ RxJS สำหรับ pipeline และแปลงที่ boundary:
// แนวทางแบบ hybrid
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
เตรียมตัวสำหรับคำถามสัมภาษณ์ Angular Signals
- Signals เป็น primitives แบบ synchronous; RxJS จัดการ async streams
- Computed signals เป็น lazy และถูก memoize คำนวณใหม่เฉพาะเมื่อ dependencies เปลี่ยน
- Effects ทำงานแบบ asynchronous ระหว่าง change detection ไม่ใช่ทันที
- linkedSignal แก้ปัญหา writable-derived-state โดยไม่ต้องใช้ effects
- Signal inputs (
input(),model()) แทนที่ decorators ด้วยทางเลือก reactive toSignalและtoObservableเชื่อมต่อ signals และ RxJS สำหรับการย้ายแบบค่อยเป็นค่อยไป- Zoneless Angular พึ่งพา signals สำหรับ fine-grained change detection
- Resource API (
rxResource) จัดการ async state แบบ declarative - คู่มือ Angular signals อย่างเป็นทางการ ยังคงเป็นข้อมูลอ้างอิงที่น่าเชื่อถือสำหรับรายละเอียด API และ best practices
คุณหาบั๊กใน Angular เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 17 กันยายน 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

คำถามสัมภาษณ์ Angular 19: Signals, SSR และแนวคิดสำคัญที่ต้องรู้
คำถามสัมภาษณ์ Angular 19 ที่พบบ่อยที่สุด: Signals, incremental hydration, zoneless change detection และ API แบบ reactive ใหม่พร้อมตัวอย่างโค้ดและคำตอบที่คาดหวัง

Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์
เรียนรู้ระบบ dependency injection ของ Angular อย่างลึกซึ้ง รวมถึงกลยุทธ์ provider, InjectionToken, injector แบบลำดับชั้น และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Angular ที่มีประสบการณ์

ไวยากรณ์ Control Flow ใน Angular 2026: @if, @for, @switch และคำถามสัมภาษณ์
เรียนรู้ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) ที่มาแทนที่ structural directive ครบถ้วนพร้อมตัวอย่างโค้ด เคล็ดลับการ migrate และคำถามสัมภาษณ์เทคนิค