Angular Resource API và httpResource: Hướng dẫn đầy đủ (Ổn định trong Angular 22)
Resource API và httpResource của Angular hiện đã ổn định kể từ Angular 22. Hướng dẫn này bao gồm resource(), rxResource(), httpResource(), helper chain() cho resource phụ thuộc, validation Zod, và các câu hỏi phỏng vấn.

Resource API và httpResource của Angular cung cấp cơ chế lấy dữ liệu dựa trên signal, tự động theo dõi các trạng thái loading, error và resolved. Được giới thiệu trong Angular 20, các API này đã trở nên ổn định trong Angular 22, thay thế các mẫu subscription HttpClient dài dòng bằng các primitive phản ứng.
Resource API (resource(), rxResource(), httpResource()) đã chuyển từ thử nghiệm sang ổn định trong Angular 22 (tháng 6 năm 2026). Cách đặt tên params và stream, các giá trị trạng thái dạng chuỗi, và helper chain() mới cho resource phụ thuộc đều đã sẵn sàng cho production. Nếu bạn đang nâng cấp từ Angular 20 hoặc 21, API vẫn tương thích.
Ba biến thể Resource
Angular 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, đây là một hạn chế 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
}
}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.
Chuỗi Resource với chain()
Angular 22 giới thiệu helper chain() cho việc tải resource phụ thuộc. Khi một resource phụ thuộc vào giá trị của resource khác, chain() tự động lan truyền các trạng thái loading và error.
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
authorId: string;
}
interface Post {
id: number;
title: string;
content: string;
}
@Component({
selector: 'app-user-posts',
template: \`
@if (postsResource.isLoading()) {
<p>Loading...</p>
} @else if (postsResource.hasValue()) {
<h2>Posts by {{ userResource.value().name }}</h2>
@for (post of postsResource.value(); track post.id) {
<article>
<h3>{{ post.title }}</h3>
</article>
}
} @else if (postsResource.error()) {
<p>Failed to load posts</p>
}
\`,
})
export class UserPostsComponent {
userId = signal(1);
userResource = httpResource<User>(() => \`/api/users/\${this.userId()}\`);
// chain() propagates userResource's status to postsResource
postsResource = httpResource<Post[]>({
params: ({ chain }) => {
const user = chain(this.userResource);
return { url: '/api/posts', params: { authorId: user.authorId } };
},
});
}chain(userResource) không chỉ trả về giá trị. Trong khi userResource đang loading, postsResource cũng báo cáo là đang loading và loader của nó không được thực thi. Nếu userResource gặp lỗi, postsResource chuyển sang trạng thái error với ResourceDependencyError mang theo lỗi từ upstream.
Điều này thay thế mẫu RxJS với các lời gọi switchMap lồng nhau bằng cách lan truyền trạng thái rõ ràng và đúng mặc định.
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
Angular đổ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
ResourceStatus là 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.
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 từ Angular 19, nơi value() trả về undefined khi lỗi.
Câu hỏi phỏng vấn Angular httpResource
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.
H: chain() xử lý lỗi từ resource upstream như thế nào?
Khi sử dụng chain(upstreamResource) trong hàm params của resource, resource phụ thuộc không thực thi loader nếu resource upstream đang loading, idle, hoặc ở trạng thái error. Nếu resource upstream gặp lỗi, resource phụ thuộc chuyển sang trạng thái error với ResourceDependencyError. Error này bao gồm dependency (resource upstream) và cause (error upstream thực sự), cho phép phân biệt giữa "yêu cầu của tôi thất bại" và "yêu cầu của dependency thất bại".
Để 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, hướng dẫn 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.
Nguồn tham khảo
- Tài liệu Angular Resource API — hướng dẫn chính thức về resource(), rxResource(), và httpResource()
- What's new in Angular 22.0 — bài viết của Ninja Squad về Resource API ổn định và helper chain()
- Hướng dẫn interceptor HTTP Angular — cách interceptor hoạt động với httpResource
Điểm chính cho Angular Resource API
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 - Helper
chain()(Angular 22+) cho phép tải resource phụ thuộc với lan truyền trạng thái tự động; nếu upstream thất bại, resource phụ thuộc nhậnResourceDependencyError - Giá trị trạng thái là các literal chuỗi (
'idle','loading','resolved','error','reloading','local') - 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ệ; 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
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 13 tháng 10, 2026
Thẻ
Chia sẻ
Bài viết liên quan

NgRx Signal Store vs Classic NgRx trong 2026: Nên Chọn Giải Pháp Nào?
So sánh toàn diện NgRx Signal Store và Classic NgRx cho quản lý state Angular. Tìm hiểu khi nào sử dụng mỗi cách tiếp cận với các ví dụ code thực tế.

RxJS trong Angular 2026: Operator, Subject và Interop Signals
RxJS trong Angular 2026: nắm vững các operator, Subject và mẫu interop Signals mà lập trình viên Angular dùng trong production, cùng những câu hỏi phỏng vấn hay gặp nhất.

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.