Angular 20 năm 2026: Resource API, httpResource và câu hỏi phỏng vấn
Angular 20 giới thiệu httpResource và ổn định hóa Resource API cho việc lấy dữ liệu dựa trên signal. Hướng dẫn thực hành về resource(), rxResource(), httpResource(), validation Zod và các câu hỏi phỏng vấn thường gặp.

Angular 20 đưa Resource API và httpResource lên hàng đầu trong việc lấy dữ liệu dựa trên signal. Các API thử nghiệm này thay thế những mẫu subscription HttpClient dài dòng bằng các primitive phản ứng tự động theo dõi các trạng thái loading, error và resolved.
Resource API đổi tên request thành params và loader thành stream (đối với rxResource). Giá trị trạng thái giờ là các literal chuỗi ('idle', 'loading', 'resolved', 'error', 'reloading', 'local') thay vì enum số. httpResource được xây dựng trên HttpClient, hỗ trợ sẵn interceptor và validation bằng Zod.
Ba biến thể Resource trong Angular 20
Angular 20 cung cấp ba cách để tải dữ liệu bất đồng bộ dưới dạng signal. Mỗi cách nhắm đến một trường hợp sử dụng khác nhau, nhưng tất cả đều chia sẻ cùng một mô hình phản ứng: khai báo các phụ thuộc, định nghĩa một loader, rồi tiêu thụ kết quả thông qua signal.
resource()hoạt động với Promise. Lý tưởng khi dùngfetch()hoặc bất kỳ API dựa trên promise nào.rxResource()hoạt động với Observable. Lựa chọn đúng đắn khi cần các toán tử RxJS nhưdebounceTime,retryhoặcswitchMap.httpResource()bao bọc trực tiếpHttpClientcủa Angular. Interceptor, các tiện ích kiểm thử và validation schema hoạt động mà không cần thiết lập thêm.
Khác biệt then chốt giữa httpResource và hai cái còn lại: httpResource dùng HttpClient bên dưới, nghĩa là các interceptor hiện có vẫn tiếp tục hoạt động. API resource() ban đầu bỏ qua hoàn toàn HttpClient, một điểm khó chịu lớn trong Angular 19.
Xây dựng hồ sơ người dùng với resource()
Hàm resource() nhận một hàm tính toán params và một hàm loader. Khi các signal bên trong params thay đổi, loader sẽ tự động chạy lại.
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
}
}Tham số abortSignal cho phép Angular hủy các yêu cầu đang chạy khi userId thay đổi trước khi yêu cầu trước đó hoàn tất. Điều này ngăn race condition mà không cần quản lý subscription thủ công.
Lấy dữ liệu phản ứng với httpResource
httpResource loại bỏ mã lặp lại bằng cách kết hợp khai báo URL và thực thi HTTP trong một lời gọi duy nhất. Nó trả về một HttpResourceRef phơi bày value, isLoading, error, status và headers dưới dạng signal.
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
}
}Có nhiều chi tiết quan trọng ở đây. Hàm truyền cho httpResource trả về một đối tượng cấu hình yêu cầu. Angular theo dõi các lần đọc signal bên trong hàm này, nên thay đổi category sẽ kích hoạt một yêu cầu GET mới. Nếu một yêu cầu đang chạy, Angular sẽ hủy nó trước khi bắt đầu yêu cầu mới.
httpResource được thiết kế cho việc lấy dữ liệu (yêu cầu GET). Dùng nó cho các thao tác POST, PUT hoặc DELETE là không an toàn, vì việc hủy yêu cầu có thể làm gián đoạn một mutation giữa chừng. Đối với thao tác ghi, hãy dùng HttpClient trực tiếp hoặc bao bọc mutation trong một method của service.
Validation schema với Zod và httpResource
Phản hồi từ các dịch vụ bên ngoài có thể lệch khỏi hình dạng mong đợi. Tùy chọn parse trên httpResource tích hợp các thư viện validation schema như Zod để bắt sự không khớp tại thời điểm runtime, thay vì âm thầm lan truyền dữ liệu hỏng.
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 }
);
}Khi API trả về dữ liệu không khớp với OrderSchema, resource chuyển sang trạng thái 'error'. Kiểu trả về của hàm parse cũng quyết định kiểu TypeScript của value(), nên các định nghĩa schema đảm nhiệm hai vai trò: vừa là validator runtime vừa là bộ sinh kiểu.
rxResource với stream và params trong Angular 20
Angular 20 đổi tên loader thành stream và request thành params trong rxResource. Thay đổi này đồng bộ cách đặt tên với ngữ nghĩa streaming mà rxResource hỗ trợ. Hàm stream nhận một ngữ cảnh Observable và phải trả về một Observable.
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 },
}),
});
}Khác với httpResource, rxResource trao toàn quyền kiểm soát pipeline Observable. Các toán tử như debounceTime hoặc retry có thể được nối chuỗi bên trong stream. Tuy nhiên, httpResource xử lý trường hợp phổ biến nhất (một yêu cầu GET đơn lẻ) với ít mã hơn.
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.
Theo dõi trạng thái: literal chuỗi thay thế enum
Angular 20 thay đổi ResourceStatus từ một enum số thành một kiểu union chuỗi. Sáu giá trị khả dĩ cung cấp cái nhìn chi tiết về vòng đời của resource:
| Trạng thái | Ý nghĩa |
|---|---|
| 'idle' | params trả về undefined — không có yêu cầu nào được gửi |
| 'loading' | Yêu cầu đầu tiên đang diễn ra |
| 'reloading' | Yêu cầu tiếp theo sau một lần thành công trước đó |
| 'resolved' | Dữ liệu có sẵn trong value() |
| 'error' | Yêu cầu thất bại — error() chứa lỗi |
| 'local' | Giá trị được đặt cục bộ qua .set() hoặc .update() |
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();
},
});
}Trả về undefined từ params đặt trạng thái thành 'idle' và ngăn loader chạy. Mẫu này phù hợp với việc lấy dữ liệu có điều kiện — hiển thị một lời nhắc cho đến khi người dùng nhập liệu, rồi tải dữ liệu.
Kể từ Angular 20, gọi value() trên một resource ở trạng thái 'error' sẽ ném ra ngoại lệ tại runtime. Luôn bảo vệ việc đọc bằng hasValue() hoặc kiểm tra status() trước khi truy cập value(). Đây là một thay đổi phá vỡ so với Angular 19, nơi value() trả về undefined khi lỗi.
Câu hỏi phỏng vấn httpResource Angular 20
Resource API đang trở thành một chủ đề tiêu chuẩn trong câu hỏi phỏng vấn Angular. Dưới đây là những câu hỏi kiểm tra sự hiểu biết thực sự, vượt ra ngoài sự quen thuộc bề mặt với API.
H: httpResource giải quyết vấn đề nào mà resource() không giải quyết được?
resource() dùng fetch() hoặc bất kỳ loader dựa trên promise nào, bỏ qua HttpClient của Angular. Điều này nghĩa là các interceptor (cho token xác thực, ghi log, xử lý lỗi) không được áp dụng. httpResource dùng HttpClient bên trong, nên các interceptor, tiện ích kiểm thử (HttpTestingController) và các tính năng như withFetch() hoạt động mà không cần cấu hình thêm.
H: Khi nào nên ưu tiên rxResource hơn httpResource?
rxResource cung cấp toàn quyền kiểm soát Observable thông qua hàm stream của nó. Hãy chọn nó khi pipeline dữ liệu cần các toán tử RxJS — debounce đầu vào tìm kiếm, thử lại các yêu cầu thất bại với exponential backoff, hoặc kết hợp nhiều stream với combineLatest. Với các yêu cầu GET đơn giản, httpResource cần ít mã hơn.
H: Angular xử lý các yêu cầu đồng thời thế nào khi một signal thay đổi nhanh chóng?
Cả ba biến thể resource đều hủy các yêu cầu đang chạy khi params tạo ra một giá trị mới. Với httpResource và resource(), AbortSignal hủy fetch bên dưới. Với rxResource, Angular hủy đăng ký khỏi Observable trước đó. Điều này ngăn các phản hồi cũ ghi đè lên dữ liệu mới.
H: Mục đích của trạng thái 'local' là gì?
Gọi .set() hoặc .update() trên một resource thay đổi giá trị của nó cục bộ mà không kích hoạt loader. Trạng thái chuyển sang 'local', cho thấy giá trị hiện tại không đến từ máy chủ. Điều này hỗ trợ cập nhật UI lạc quan — UI phản ánh thay đổi ngay lập tức trong khi một yêu cầu mutation riêng biệt đang chạy.
H: Tích hợp Zod hoạt động thế nào với httpResource?
Tùy chọn parse chấp nhận bất kỳ hàm nào có chữ ký (data: unknown) => T. Khi phản hồi HTTP đến, httpResource truyền JSON đã phân tích qua parse trước khi đặt value(). Nếu parse ném ngoại lệ (ví dụ ZodError), resource chuyển sang trạng thái 'error'. Kiểu trả về của parse quyết định kiểu TypeScript của value(), mang lại cả an toàn runtime và kiểu tại thời điểm biên dịch từ một định nghĩa schema duy nhất.
Để tìm hiểu sâu hơn về signal Angular và cách chúng tích hợp với framework rộng hơn, module signal bao quát computed signal, effect và mô hình phản ứng.
Di chuyển từ subscription HttpClient sang httpResource
Các ứng dụng Angular hiện có thường lấy dữ liệu bằng subscription HttpClient bên trong ngOnInit, hoặc dùng AsyncPipe với Observable. Việc di chuyển sang httpResource tuân theo một mẫu có thể đoán trước:
// 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()
}Việc di chuyển loại bỏ mã lặp lại quản lý vòng đời. Việc dọn dẹp subscription diễn ra tự động khi component bị hủy. Các trạng thái loading và error được tích hợp sẵn trong resource — không cần các cờ boolean riêng.
Đối với các ứng dụng đã dùng standalone component, việc di chuyển rất đơn giản: thay thế việc inject HttpClient và logic subscription bằng một khai báo httpResource duy nhất.
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.
Kết luận
httpResourcethay thế các subscriptionHttpClientthủ công bằng một khai báo phản ứng duy nhất tự động xử lý loading, error và hủy yêu cầu- Dùng
resource()cho các API dựa trên promise,rxResource()cho các pipeline Observable với toán tử RxJS, vàhttpResource()cho các lời gọi HTTP tiêu chuẩn có hỗ trợ interceptor - Angular 20 đổi tên
requestthànhparamsvàloaderthànhstreamtrongrxResource— hãy cập nhật mã hiện có cho phù hợp - Giá trị trạng thái giờ là các literal chuỗi (
'idle','loading','resolved','error','reloading','local'), thay thế enum số từ Angular 19 - Tùy chọn
parsetrênhttpResourcetích hợp Zod hoặc Valibot cho validation schema runtime đồng thời điều khiển các kiểu TypeScript - Gọi
value()trên một resource ở trạng thái lỗi sẽ ném ngoại lệ trong Angular 20 — luôn bảo vệ bằnghasValue()hoặc kiểm trastatus()trước - Tất cả các biến thể resource đều tự động hủy các yêu cầu đang chạy khi các phụ thuộc thay đổi, ngăn race condition mà không cần logic hủy thủ công
Thẻ
Chia sẻ
Bài viết liên quan

Angular Standalone Components: Huong dan di chuyen va cac phuong phap tot nhat 2026
Huong dan chi tiet di chuyen Angular standalone components. Quy trinh tung buoc xoa NgModules, kich hoat lazy loading va ap dung standalone API trong Angular 21.

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.

Angular 19 Zoneless: Hiệu năng và Change Detection không cần Zone.js
Angular zoneless change detection loại bỏ Zone.js để mang lại bundle nhỏ hơn, rendering nhanh hơn và mô hình reactivity tường minh thông qua signals. Hướng dẫn chuyên sâu về quá trình chuyển đổi từ Zone.js sang zoneless Angular, từ API experimental trong Angular 19 đến API stable trong Angular 20+.