Angular Signals และ Computed ในปี 2026: Fine-Grained Reactivity และคำถามสัมภาษณ์

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

Angular Signals และ Computed - Fine-Grained Reactivity ในปี 2026

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 ที่ขึ้นอยู่กับมันจะได้รับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลง

counter.component.tstypescript
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 ด้วยตนเอง

user-stats.component.tstypescript
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 ทั้งหมดเสร็จสิ้น

theme-sync.component.tstypescript
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 ที่อ่านเฉพาะในสาขานั้นจะไม่ถูกติดตามจนกว่าเงื่อนไขจะเปลี่ยน

Anti-Pattern: Effect สำหรับการสร้าง State

การใช้ effect เพื่อคัดลอกข้อมูลจาก signal หนึ่งไปยังอีก signal บ่งชี้ปัญหาในการออกแบบ ควรใช้ computed สำหรับ derived state หรือ linkedSignal เมื่อค่าที่สร้างขึ้นต้องสามารถเขียนได้

linkedSignal: Writable Derived State

ถูกแนะนำใน Angular 19 และเสถียรใน Angular 20 linkedSignal สร้าง writable signal ที่รีเซ็ตเมื่อแหล่งที่มาเปลี่ยน มันแก้ไขสถานการณ์ที่ computed เป็น read-only แต่ค่าที่สร้างขึ้นต้องการการแก้ไขในท้องถิ่น

pagination.component.tstypescript
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

user-card.component.tstypescript
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 requestsRxJS Observable + toSignal()
WebSocket streamsRxJS พร้อม operators
Input ของผู้ใช้ที่มี debounceRxJS debounceTime แล้ว toSignal()
การประสานงาน async ที่ซับซ้อนRxJS pipelines

RxJS ยังคงเป็นเครื่องมือสำหรับ async streams, cancellation และ backpressure Signals จัดการ state แบบ synchronous

การทำงานร่วมกับ RxJS: toSignal และ toObservable

Angular มี utilities ใน @angular/core/rxjs-interop สำหรับเชื่อมต่อ signals และ observables

search.component.tstypescript
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 ในเงื่อนไขอย่างไร?

typescript
// ตัวอย่างการติดตาม 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 ได้:

typescript
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

zoneless-counter.component.tstypescript
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

user-profile.component.tstypescript
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:

cart.service.tstypescript
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 อยู่ร่วมกัน

typescript
// ก่อน: ใช้ 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:

typescript
// แนวทางแบบ 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

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 17 กันยายน 2569

แท็ก

#angular
#signals
#computed
#reactivity
#interview

แชร์

บทความที่เกี่ยวข้อง