Angular 20 ในปี 2026: Resource API, httpResource และคำถามสัมภาษณ์

Angular 20 เปิดตัว httpResource และทำให้ Resource API เสถียรสำหรับการดึงข้อมูลแบบอิงกับ signal บทเรียนเชิงปฏิบัติครอบคลุม resource(), rxResource(), httpResource(), การตรวจสอบด้วย Zod และคำถามสัมภาษณ์ที่พบบ่อย

บทเรียน Angular 20 Resource API และ httpResource สำหรับการดึงข้อมูลเชิงปฏิกิริยาด้วย signal

Angular 20 ผลักดัน Resource API และ httpResource ขึ้นมาเป็นแกนหลักของการดึงข้อมูลแบบอิงกับ signal API เชิงทดลองเหล่านี้เข้ามาแทนรูปแบบการ subscribe HttpClient ที่เยิ่นเย้อ ด้วย primitive เชิงปฏิกิริยาที่ติดตามสถานะ loading, error และ resolved ให้โดยอัตโนมัติ

สิ่งที่เปลี่ยนไปใน Angular 20

Resource API เปลี่ยนชื่อ request เป็น params และ loader เป็น stream (สำหรับ rxResource) ค่าสถานะตอนนี้เป็น string literal ('idle', 'loading', 'resolved', 'error', 'reloading', 'local') แทนที่ enum ที่เป็นตัวเลข httpResource สร้างขึ้นบน HttpClient รองรับ interceptor และการตรวจสอบด้วย Zod ได้ทันที

สาม Resource ใน Angular 20

Angular 20 มีสามวิธีในการโหลดข้อมูลแบบ asynchronous ในรูปของ signal แต่ละวิธีเหมาะกับกรณีใช้งานที่ต่างกัน แต่ทั้งหมดใช้โมเดลเชิงปฏิกิริยาแบบเดียวกัน คือ ประกาศ dependency กำหนด loader แล้วบริโภคผลลัพธ์ผ่าน signal

  • resource() ทำงานกับ Promise เหมาะอย่างยิ่งเมื่อใช้ fetch() หรือ API ใด ๆ ที่อิงกับ promise
  • rxResource() ทำงานกับ Observable เป็นตัวเลือกที่ถูกต้องเมื่อจำเป็นต้องใช้ operator ของ RxJS เช่น debounceTime, retry หรือ switchMap
  • httpResource() ห่อหุ้ม HttpClient ของ Angular โดยตรง interceptor เครื่องมือสำหรับทดสอบ และการตรวจสอบ schema ทำงานได้โดยไม่ต้องตั้งค่าเพิ่ม

ความแตกต่างสำคัญระหว่าง httpResource กับอีกสองตัว คือ httpResource ใช้ HttpClient อยู่เบื้องหลัง ซึ่งหมายความว่า interceptor ที่มีอยู่เดิมยังคงทำงานต่อไป ส่วน API resource() ดั้งเดิมข้าม HttpClient ไปทั้งหมด ซึ่งเป็นจุดอ่อนสำคัญใน Angular 19

สร้างโปรไฟล์ผู้ใช้ด้วย resource()

ฟังก์ชัน resource() รับฟังก์ชันคำนวณ params และฟังก์ชัน loader เมื่อ signal ภายใน params เปลี่ยน loader จะถูกเรียกทำงานใหม่โดยอัตโนมัติ

user-profile.component.tstypescript
import { Component, signal, resource } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  template: `
    @if (userResource.hasValue()) {
      <h2>{{ userResource.value().name }}</h2>
      <p>{{ userResource.value().email }}</p>
    } @else if (userResource.isLoading()) {
      <p>Loading profile...</p>
    } @else if (userResource.error()) {
      <p>Failed to load user</p>
    }
  `,
})
export class UserProfileComponent {
  userId = signal(1);

  // params produces the reactive dependency
  // loader receives it and returns a Promise
  userResource = resource<User, number>({
    params: () => this.userId(),
    loader: async ({ params: id, abortSignal }) => {
      const res = await fetch(`/api/users/${id}`, { signal: abortSignal });
      return res.json();
    },
  });

  loadUser(id: number) {
    this.userId.set(id); // triggers automatic refetch
  }
}

พารามิเตอร์ abortSignal ช่วยให้ Angular ยกเลิกคำขอที่กำลังทำงานอยู่ได้ เมื่อ userId เปลี่ยนก่อนที่คำขอก่อนหน้าจะเสร็จสิ้น สิ่งนี้ป้องกัน race condition โดยไม่ต้องจัดการ subscription ด้วยตนเอง

ดึงข้อมูลเชิงปฏิกิริยาด้วย httpResource

httpResource ขจัดโค้ดซ้ำซากด้วยการรวมการประกาศ URL และการเรียก HTTP ไว้ในการเรียกครั้งเดียว มันคืนค่า HttpResourceRef ที่เปิดเผย value, isLoading, error, status และ headers ในรูปของ signal

product-list.component.tstypescript
import { Component, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

@Component({
  selector: 'app-product-list',
  template: `
    @if (products.hasValue()) {
      @for (product of products.value(); track product.id) {
        <div class="product-card">
          <h3>{{ product.name }}</h3>
          <span>{{ product.price | currency }}</span>
        </div>
      }
    } @else if (products.isLoading()) {
      <p>Loading products...</p>
    }
  `,
})
export class ProductListComponent {
  category = signal('electronics');

  // httpResource re-fetches whenever category() changes
  products = httpResource<Product[]>(() => ({
    url: '/api/products',
    params: { category: this.category() },
  }));

  filterByCategory(cat: string) {
    this.category.set(cat); // pending request is cancelled, new one starts
  }
}

มีรายละเอียดหลายอย่างที่สำคัญตรงนี้ ฟังก์ชันที่ส่งให้ httpResource คืนค่าออบเจ็กต์การตั้งค่าคำขอ Angular ติดตามการอ่าน signal ภายในฟังก์ชันนี้ ดังนั้นการเปลี่ยน category จึงกระตุ้นคำขอ GET ใหม่ หากมีคำขอกำลังทำงานอยู่ Angular จะยกเลิกมันก่อนเริ่มคำขอใหม่

httpResource ใช้สำหรับการอ่านเท่านั้น

httpResource ออกแบบมาสำหรับการดึงข้อมูล (คำขอ GET) การนำไปใช้กับการดำเนินการ POST, PUT หรือ DELETE นั้นไม่ปลอดภัย เพราะการยกเลิกคำขออาจยกเลิก mutation กลางคันได้ สำหรับการดำเนินการเขียน ให้ใช้ HttpClient โดยตรงหรือห่อ mutation ไว้ในเมธอดของ service

ตรวจสอบ schema ด้วย Zod และ httpResource

การตอบกลับจากบริการภายนอกอาจเบี่ยงเบนจากรูปร่างที่คาดหวัง ตัวเลือก parse บน httpResource ผสานไลบรารีตรวจสอบ schema อย่าง Zod เพื่อจับความไม่ตรงกัน ณ ขณะรันไทม์ แทนที่จะปล่อยให้ข้อมูลที่เสียหายแพร่กระจายไปเงียบ ๆ

order.component.tstypescript
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { z } from 'zod';

// Define the expected shape with Zod
const OrderSchema = z.object({
  id: z.number(),
  status: z.enum(['pending', 'shipped', 'delivered', 'cancelled']),
  total: z.number().positive(),
  items: z.array(z.object({
    productId: z.number(),
    quantity: z.number().int().positive(),
    unitPrice: z.number().positive(),
  })),
  createdAt: z.string().datetime(),
});

type Order = z.infer<typeof OrderSchema>;

@Component({
  selector: 'app-order',
  template: `
    @if (order.hasValue()) {
      <h2>Order #{{ order.value().id }}</h2>
      <p>Status: {{ order.value().status }}</p>
      <p>Total: {{ order.value().total | currency }}</p>
    } @else if (order.error()) {
      <p>Invalid order data received</p>
    }
  `,
})
export class OrderComponent {
  orderId = signal(42);

  // parse validates the response before exposing it as a signal
  order = httpResource<Order>(
    () => `/api/orders/${this.orderId()}`,
    { parse: OrderSchema.parse }
  );
}

เมื่อ API คืนค่าข้อมูลที่ไม่ตรงกับ OrderSchema resource จะเปลี่ยนเป็นสถานะ 'error' ชนิดของค่าที่ฟังก์ชัน parse คืนกลับมายังกำหนดชนิด TypeScript ของ value() ด้วย ดังนั้นนิยาม schema จึงทำหน้าที่สองอย่าง ทั้งเป็นตัวตรวจสอบขณะรันไทม์และตัวสร้างชนิด

rxResource ด้วย stream และ params ใน Angular 20

Angular 20 เปลี่ยนชื่อ loader เป็น stream และ request เป็น params ใน rxResource การเปลี่ยนแปลงนี้ทำให้การตั้งชื่อสอดคล้องกับความหมายของ streaming ที่ rxResource รองรับ ฟังก์ชัน stream รับบริบท Observable และต้องคืนค่า Observable

search.component.tstypescript
import { Component, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { debounceTime, switchMap } from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
  excerpt: string;
}

@Component({
  selector: 'app-search',
  template: `
    <input (input)="query.set($any($event.target).value)" placeholder="Search..." />
    @if (results.isLoading()) {
      <p>Searching...</p>
    }
    @if (results.hasValue()) {
      @for (item of results.value(); track item.id) {
        <div>{{ item.title }}</div>
      }
    }
  `,
})
export class SearchComponent {
  private http = inject(HttpClient);
  query = signal('');

  // params (was "request") provides the reactive input
  // stream (was "loader") returns an Observable
  results = rxResource<SearchResult[], string>({
    params: () => this.query(),
    stream: ({ params: q }) =>
      this.http.get<SearchResult[]>('/api/search', {
        params: { q },
      }),
  });
}

ต่างจาก httpResource ตรงที่ rxResource ให้การควบคุม pipeline ของ Observable อย่างเต็มที่ operator อย่าง debounceTime หรือ retry สามารถต่อเชื่อมไว้ภายใน stream ได้ อย่างไรก็ตาม httpResource จัดการกรณีที่พบบ่อยที่สุด (คำขอ GET เดียว) ด้วยโค้ดที่น้อยกว่า

พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การติดตามสถานะ: string literal แทนที่ enum

Angular 20 เปลี่ยน ResourceStatus จาก enum ตัวเลขเป็นชนิด union ของ string ค่าที่เป็นไปได้ทั้งหกให้มุมมองโดยละเอียดเกี่ยวกับวงจรชีวิตของ resource

| สถานะ | ความหมาย | |---|---| | 'idle' | params คืนค่า undefined — ไม่มีการส่งคำขอ | | 'loading' | คำขอแรกกำลังดำเนินการ | | 'reloading' | คำขอครั้งต่อไปหลังจากสำเร็จก่อนหน้านี้ | | 'resolved' | มีข้อมูลพร้อมใช้ใน value() | | 'error' | คำขอล้มเหลว — error() บรรจุข้อผิดพลาด | | 'local' | ตั้งค่าในเครื่องผ่าน .set() หรือ .update() |

status-demo.component.tstypescript
import { Component, signal, resource } from '@angular/core';

@Component({
  selector: 'app-status-demo',
  template: `
    <p>Status: {{ data.status() }}</p>
    @switch (data.status()) {
      @case ('loading') { <spinner /> }
      @case ('reloading') { <subtle-spinner /> }
      @case ('resolved') { <data-table [rows]="data.value()" /> }
      @case ('error') { <error-banner [error]="data.error()" /> }
      @case ('idle') { <p>Select a filter to load data</p> }
    }
  `,
})
export class StatusDemoComponent {
  filter = signal<string | undefined>(undefined);

  data = resource({
    params: () => this.filter(),
    loader: async ({ params: f, abortSignal }) => {
      const res = await fetch(`/api/data?filter=${f}`, { signal: abortSignal });
      return res.json();
    },
  });
}

การคืนค่า undefined จาก params ตั้งสถานะเป็น 'idle' และป้องกันไม่ให้ loader ทำงาน รูปแบบนี้เหมาะกับการดึงข้อมูลแบบมีเงื่อนไข แสดงข้อความแจ้งจนกว่าผู้ใช้จะป้อนข้อมูล แล้วจึงโหลดข้อมูล

การเข้าถึง value() ในสถานะ error

ตั้งแต่ Angular 20 การเรียก value() บน resource ที่อยู่ในสถานะ 'error' จะโยน exception ขณะรันไทม์ ควรป้องกันการอ่านด้วย hasValue() เสมอ หรือตรวจสอบ status() ก่อนเข้าถึง value() นี่เป็น breaking change จาก Angular 19 ที่ value() คืนค่า undefined เมื่อเกิดข้อผิดพลาด

คำถามสัมภาษณ์ httpResource ของ Angular 20

Resource API กำลังกลายเป็นหัวข้อมาตรฐานใน คำถามสัมภาษณ์ Angular ต่อไปนี้คือคำถามที่ทดสอบความเข้าใจที่แท้จริง เกินกว่าความคุ้นเคยแบบผิวเผินกับ API

ถาม: httpResource แก้ปัญหาอะไรที่ resource() แก้ไม่ได้?

resource() ใช้ fetch() หรือ loader ใด ๆ ที่อิงกับ promise โดยข้าม HttpClient ของ Angular นั่นหมายความว่า interceptor (สำหรับ token การยืนยันตัวตน การบันทึก log การจัดการข้อผิดพลาด) จะไม่ถูกนำมาใช้ ส่วน httpResource ใช้ HttpClient ภายใน ดังนั้น interceptor เครื่องมือทดสอบ (HttpTestingController) และฟีเจอร์อย่าง withFetch() จึงทำงานได้โดยไม่ต้องตั้งค่าเพิ่มเติม

ถาม: ควรเลือก rxResource แทน httpResource เมื่อใด?

rxResource ให้การควบคุม Observable อย่างเต็มที่ผ่านฟังก์ชัน stream เลือกใช้เมื่อ pipeline ข้อมูลต้องการ operator ของ RxJS เช่น การ debounce การป้อนค้นหา การลองคำขอที่ล้มเหลวซ้ำด้วย exponential backoff หรือการรวมหลาย stream ด้วย combineLatest สำหรับคำขอ GET ทั่วไป httpResource ใช้โค้ดน้อยกว่า

ถาม: Angular จัดการคำขอพร้อมกันอย่างไรเมื่อ signal เปลี่ยนอย่างรวดเร็ว?

ทั้งสาม resource ยกเลิกคำขอที่กำลังทำงานเมื่อ params สร้างค่าใหม่ สำหรับ httpResource และ resource() AbortSignal ยกเลิก fetch ที่อยู่เบื้องหลัง สำหรับ rxResource Angular ยกเลิกการ subscribe จาก Observable ก่อนหน้า สิ่งนี้ป้องกันไม่ให้การตอบกลับที่ล้าสมัยเขียนทับข้อมูลใหม่

ถาม: สถานะ 'local' มีไว้เพื่ออะไร?

การเรียก .set() หรือ .update() บน resource เปลี่ยนค่าในเครื่องโดยไม่กระตุ้น loader สถานะเปลี่ยนเป็น 'local' ซึ่งบ่งบอกว่าค่าปัจจุบันไม่ได้มาจากเซิร์ฟเวอร์ สิ่งนี้รองรับการอัปเดต UI แบบ optimistic โดย UI สะท้อนการเปลี่ยนแปลงทันทีในขณะที่คำขอ mutation แยกต่างหากกำลังทำงานอยู่

ถาม: การผสาน Zod ทำงานกับ httpResource อย่างไร?

ตัวเลือก parse รับฟังก์ชันใด ๆ ที่มี signature (data: unknown) => T เมื่อการตอบกลับ HTTP มาถึง httpResource ส่ง JSON ที่แปลงแล้วผ่าน parse ก่อนตั้งค่า value() หาก parse โยน exception (เช่น ZodError) resource จะเปลี่ยนเป็นสถานะ 'error' ชนิดที่ parse คืนค่ากำหนดชนิด TypeScript ของ value() ให้ทั้งความปลอดภัยขณะรันไทม์และชนิดขณะคอมไพล์จากนิยาม schema เดียว

หากต้องการเจาะลึกเรื่อง signal ของ Angular และวิธีที่พวกมันผสานเข้ากับเฟรมเวิร์กในวงกว้าง โมดูล signal ครอบคลุม computed signal, effect และโมเดลเชิงปฏิกิริยา

ย้ายจาก subscription ของ HttpClient ไปยัง httpResource

แอปพลิเคชัน Angular ที่มีอยู่มักดึงข้อมูลด้วย subscription ของ HttpClient ภายใน ngOnInit หรือใช้ AsyncPipe กับ Observable การย้ายไปยัง httpResource เป็นไปตามรูปแบบที่คาดเดาได้

typescript
// BEFORE: manual subscription in ngOnInit
@Component({ /* ... */ })
export class BeforeComponent implements OnInit, OnDestroy {
  private http = inject(HttpClient);
  private destroy$ = new Subject<void>();
  users: User[] = [];
  loading = false;
  error: string | null = null;

  ngOnInit() {
    this.loading = true;
    this.http.get<User[]>('/api/users')
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (data) => { this.users = data; this.loading = false; },
        error: (err) => { this.error = err.message; this.loading = false; },
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// AFTER: httpResource handles lifecycle automatically
@Component({ /* ... */ })
export class AfterComponent {
  users = httpResource<User[]>(() => '/api/users');
  // No ngOnInit, no Subject, no manual unsubscribe
  // Template uses users.value(), users.isLoading(), users.error()
}

การย้ายนี้ขจัดโค้ดซ้ำซากในการจัดการวงจรชีวิต การล้าง subscription เกิดขึ้นโดยอัตโนมัติเมื่อ component ถูกทำลาย สถานะ loading และ error ถูกฝังอยู่ใน resource อยู่แล้ว ไม่จำเป็นต้องมี flag แบบ boolean แยกต่างหาก

สำหรับแอปพลิเคชันที่ใช้ standalone component อยู่แล้ว การย้ายทำได้ตรงไปตรงมา เพียงแทนที่การ inject HttpClient และตรรกะ subscription ด้วยการประกาศ httpResource เพียงรายการเดียว

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

บทสรุป

  • httpResource แทนที่ subscription ของ HttpClient ที่ทำเองด้วยการประกาศเชิงปฏิกิริยาเพียงรายการเดียว ที่จัดการ loading, error และการยกเลิกโดยอัตโนมัติ
  • ใช้ resource() สำหรับ API ที่อิงกับ promise, rxResource() สำหรับ pipeline ของ Observable ที่มี operator ของ RxJS และ httpResource() สำหรับการเรียก HTTP มาตรฐานที่รองรับ interceptor
  • Angular 20 เปลี่ยนชื่อ request เป็น params และ loader เป็น stream ใน rxResource — ปรับปรุงโค้ดที่มีอยู่ให้สอดคล้อง
  • ค่าสถานะตอนนี้เป็น string literal ('idle', 'loading', 'resolved', 'error', 'reloading', 'local') แทนที่ enum ตัวเลขจาก Angular 19
  • ตัวเลือก parse บน httpResource ผสาน Zod หรือ Valibot สำหรับการตรวจสอบ schema ขณะรันไทม์ที่ขับเคลื่อนชนิด TypeScript ด้วย
  • การเรียก value() บน resource ในสถานะ error จะโยน exception ใน Angular 20 — ป้องกันด้วย hasValue() เสมอ หรือตรวจสอบ status() ก่อน
  • resource ทุกแบบยกเลิกคำขอที่กำลังทำงานโดยอัตโนมัติเมื่อ dependency เปลี่ยน ป้องกัน race condition โดยไม่ต้องมีตรรกะการยกเลิกด้วยตนเอง

แท็ก

#angular
#angular-20
#resource-api
#httpresource
#signals
#typescript
#tutorial

แชร์

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

Angular Standalone Components Migration Guide 2026

Angular Standalone Components: คู่มือการย้ายระบบและแนวทางปฏิบัติที่ดีที่สุดในปี 2026

คู่มือการย้ายระบบ Angular standalone components อย่างละเอียด ขั้นตอนการลบ NgModules เปิดใช้งาน lazy loading และนำ standalone API มาใช้ใน Angular 21

คำถามสัมภาษณ์ Angular 19: Signals, SSR และ incremental hydration

คำถามสัมภาษณ์ Angular 19: Signals, SSR และแนวคิดสำคัญที่ต้องรู้

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

แผนภาพสถาปัตยกรรม Angular zoneless change detection แสดง signals และการเพิ่มประสิทธิภาพ

Angular 19 Zoneless: ประสิทธิภาพและ Change Detection โดยไม่ต้องใช้ Zone.js

Angular zoneless change detection ลบ Zone.js ออกเพื่อให้ได้ bundle ที่เล็กลง rendering ที่เร็วขึ้น และ reactivity ที่ชัดเจนผ่าน signals คู่มือเชิงลึกเกี่ยวกับการย้ายจาก Zone.js ไปยัง zoneless Angular ตั้งแต่ API experimental ใน Angular 19 จนถึง API stable ใน Angular 20+