今天來看 Angular Signal 裡一個很重要的非同步資料 API:Resource API。
前面介紹 Signal 時,我們處理的大多都是同步狀態,例如:
count = signal(0);
資料改變之後,Angular 可以透過 Signal 的依賴關係知道哪些地方受到影響。
不過實際開發不可能只有同步資料。像是從 API 取得使用者資料時,我們除了關心「最後拿到什麼資料」,通常還需要知道目前是不是還在載入,以及過程中有沒有發生錯誤。
當查詢條件改變時,也可能需要重新取得資料;如果前一次請求還沒完成,還得考慮這筆舊請求該怎麼處理。這些其實都屬於非同步資料在載入過程中的一部分。
以前在 Angular 裡,我們通常會透過 HttpClient 發出請求,再搭配 Observable 取得 API 回傳的資料。
實際開發時,通常會把 API 請求放進 Service,例如:
import { inject, Service } from '@angular/core';
import { HttpClient } from '@angular/common/http';
const API_URL = 'https://jsonplaceholder.typicode.com/users/1';
export interface User {
id: number;
name: string;
email: string;
}
@Service()
export class UserService {
private http = inject(HttpClient);
getUser() {
return this.http.get<User>(API_URL);
}
}
Component 再訂閱這個 Observable,除了把 API 回傳的資料存下來,也得自己處理 loading、error,以及請求失敗後重新載入的邏輯:
import { Component, inject, signal } from '@angular/core';
import { User, UserService } from './user.service';
@Component({
selector: 'app-root',
template: `
@if (loading()) {
<p>載入中...</p>
}
@if (error()) {
<p>{{ error() }}</p>
<button (click)="loadUser()">重新載入</button>
}
@if (user(); as user) {
<p>姓名:{{ user.name }}</p>
<p>Email:{{ user.email }}</p>
}
`,
})
export class AppComponent {
private userService = inject(UserService);
user = signal<User | null>(null);
loading = signal(false);
error = signal<string | null>(null);
constructor() {
this.loadUser();
}
loadUser() {
this.loading.set(true);
this.error.set(null);
this.userService.getUser().subscribe({
next: (user) => {
this.user.set(user);
this.loading.set(false);
},
error: () => {
this.error.set('取得使用者資料失敗');
this.loading.set(false);
},
});
}
}
這種寫法本身沒有問題,不過 Component 除了真正需要的 user 資料之外,還需要自己管理 loading、error、重新載入,以及 API 成功或失敗後的狀態切換。
一兩個請求的時候可能還不明顯,但當頁面上的非同步資料越來越多,Component 很容易開始同時處理畫面邏輯和資料載入的狀態。

而 Resource API 想處理的,就是這類問題。
Signal 本身是一套同步的響應式狀態模型,而 Resource 則是在這套模型上補上非同步資料的處理。除了保存最後取得的資料,也會一起管理資料目前的載入狀態。
Resource 本身不限定資料一定要從 HTTP 取得,資料可以來自 fetch、HttpClient、IndexedDB,或其他 Promise-based 的非同步操作。
它比較關心的是「這筆資料怎麼取得」以及「什麼條件改變時需要重新取得」。這裡先從三個比較重要的部分開始看:loader、params 和 abortSignal。
loader 負責執行非同步操作,並回傳最後取得的結果。
例如使用 fetch:
const userResource = resource({
loader: () =>
fetch('/api/user').then((response) => response.json()),
});
如果原本使用的是 HttpClient,因為 HttpClient 回傳的是 Observable,需要先轉成 Promise:
const userResource = resource({
loader: () => firstValueFrom(this.userService.getUser()),
});
resource() 並不限制資料一定要從哪裡取得,只要 loader 最後能提供非同步結果即可。資料取得完成後,Resource 會保存結果,並一起管理這筆資料目前的載入狀態。
補充:
firstValueFrom()只是把 Observable 轉成 Promise,Resource 提供的取消訊號不會因此自動傳給HttpClient。如果資料來源本身就是 HTTP,也可以使用專門整合 Angular HTTP 請求的httpResource()。
實際開發時,我們通常不會永遠取得同一筆資料。例如使用者切換不同的 userId 時,就可以透過 params 建立 Resource 的響應式參數:
userId = signal(1);
userResource = resource({
params: () => ({
id: this.userId(),
}),
loader: ({ params }) => {
return fetch(`/api/users/${params.id}`)
.then((response) => response.json());
},
});
這裡的 params 不只是把 id 傳進 loader,它本身也是一段響應式計算。
因為 callback function 裡讀取了 this.userId(),Angular 會在執行時追蹤這個 Signal。這部分也和前面介紹過的 Reactive Context 有關。
當我們修改:
this.userId.set(2);
params 會根據新的 userId 重新計算,Resource 接著使用新的參數再次執行 loader。
以前如果自己處理這段流程,可能會在修改 userId 後,再手動呼叫一次載入資料的方法:
this.userId.set(2);
this.loadUser();
使用 Resource 後,重新載入的條件已經放進 params 裡。只需要修改 Signal,就能根據新的參數重新取得資料。
當 params 改變後,Resource 會開始新的載入流程,不過前一次的非同步操作可能還沒有完成。
假設 userId 很快從 1 變成 2:
userId = 1
→ request A
userId = 2
→ request B
request A 比較早發出,不代表它一定會比較早完成。如果最後變成 request B 先回來,而 request A 比較晚才完成,就可能出現:
request B 完成
→ 取得 User 2
request A 後完成
→ 舊的 User 1 才回來
這就是非同步流程裡常見的 Race Condition。
Resource 在開始新的 loading operation 時,會取消前一次還沒完成的 loading operation,並透過 abortSignal 把取消訊號傳進 loader。
例如 fetch 本身就支援 AbortSignal:
userId = signal(1);
userResource = resource({
params: () => ({
id: this.userId(),
}),
loader: ({ params, abortSignal }) => {
return fetch(`/api/users/${params.id}`, {
signal: abortSignal,
}).then((response) => response.json());
},
});
這裡可以把 abortSignal 理解成 Resource 提供給底層非同步操作的「取消通知」。
當 userId 改變,Resource 準備使用新的參數重新載入資料時,如果前一次的操作還沒有完成,就會觸發這個訊號。
像 fetch 本身支援 AbortSignal,只要把它傳進 signal:
signal: abortSignal
Resource 取消這次載入時,對應的 HTTP request 就能跟著被中止,也不需要自己另外建立:
new AbortController();
不過 abortSignal 本身只是提供取消訊號,底層的非同步操作能不能真的停止,還是要看使用的 API 有沒有支援 AbortSignal。
看到這裡,可以把 resource() 裡幾個重要角色放在一起看:

loader 負責真正取得資料,params 負責描述這筆資料依賴哪些響應式條件;當條件改變、需要重新載入時,abortSignal 則提供取消前一次非同步操作的方式。
Resource 管理的不只是最後取得的資料,它也把「資料怎麼取得」、「什麼條件改變時需要重新取得」,以及「前一次載入還沒完成時該怎麼處理」放進同一套模型裡。
這也是 Resource 和單純使用 Promise 比較不一樣的地方。Promise 比較偏向描述一次非同步操作的結果,而 Resource 還會跟著它依賴的 Signal 變化,重新取得對應的資料,並管理這筆資料目前的載入狀態。
resource() 本身是一個比較通用的 API,並不是專門為 HTTP request 設計的。如果資料來源本身是 Observable,Angular 也提供了 rxResource(),可以直接和 RxJS stream 整合。
到這裡可以先記住,Resource API 的重點不只是幫我們執行一次非同步操作,而是讓非同步資料也能進入 Signal 的響應式模型。當依賴條件改變時,資料的重新取得與前一次載入的取消,也能一起交給 Resource 管理。
整理下來,Resource API 改變的其實是我們描述資料的方式。以前想的是「什麼時候要發這個請求」,所以載入時機、狀態與重試都得自己安排;改用 Resource 之後,要想的變成「這筆資料依賴哪些條件」,而資料什麼時候需要重新取得,就交給這些依賴關係來決定。
不過它也不是所有非同步操作的解法。像送出表單、刪除資料這類由使用者動作觸發的操作,並不適合因為條件改變就自動重跑一次;Resource 處理的比較好的,是「條件變了就該重新取得」的讀取型資料。
params 與 loader 的關係、loader 收到的 params/previous/abortSignal 三個參數,以及 params 在載入途中改變時會中止還沒完成的 loading operation。resource() 可以傳入的選項,以及它回傳的 Resource 介面。httpResource() 建立在 HttpClient 之上,依賴的 Signal 改變時會直接發出新的請求,並取消前一個還在進行中的請求。HttpClient 搭配 Observable 的傳統做法,對應本文前半段自己管理 loading 與 error 的寫法。