Angular Advent Calendar 2025 の 7日目の記事です。
Angular では現在、実験的機能として非同期シグナルが提供されています。それが、
• resource()
• httpResource()
の2つです。
どちらも「非同期データをシグナル化して UI に反映する」という点では似ていますが、どのような違いがあるのか気になりました。
この記事では Qiita API を使って、それぞれの違いや使い分けをコード例とともに解説します。
🚀 この記事での目標
- Resource / HttpResource の基本的な使い方
- それぞれの挙動(自動リロード・パラメータ監視の仕組み)
- 実際に使ってみて感じた「向き・不向き」
- テスト方法の違い
- 最終的な使い分け方針
Resource ― 任意の非同期処理を扱える万能シグナル
Angular には signal() や computed() といった同期シグナルがありますが、
resource() を使うと 非同期処理(Promise)もシグナルとして扱えるようになります。
特に便利なのは、
- HTTP 結果を直接シグナルとしてUIに反映したいとき
- パラメータ変更時に再取得したいとき
などです。
Resource の使い方
以下はユーザーIDに応じて Qiita の投稿一覧を取得する例です。
export class Qiita {
userId = signal<string>(''); // ユーザIDのシグナル
// ユーザIDに応じて URL を生成
url = computed(() => {
return this.userId() === ''
? `https://qiita.com/api/v2/items`
: `https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3A${this.userId()}`;
});
// Resource による非同期データ取得
itemResource = resource({
params: () => ({ url: this.url() }), // URL の変更を監視対象にする
loader: ({ params }) =>
firstValueFrom(this.getItems(params.url)), // loader は Promise を返す必要あり
});
constructor(private httpClient: HttpClient) {}
/**
* Qiita API から投稿を取得
*/
getItems(url: string) {
return this.httpClient.get(url, {
headers: { Authorization: 'Bearer ********' },
})
.pipe(
map((json: any[]) =>
json.map(row => ({
id: row.id,
title: row.title,
userName: row.user.name,
} satisfies Item))
)
);
}
}
HTML 側(Resource の状態に応じて描画)
userId: <input type="text" [(ngModel)]="userId">
<section>
<h2>Qiita Items from resource()</h2>
@if (!itemResource.isLoading() && itemResource.hasValue()) {
<ul>
@for (item of itemResource.value(); track item) {
<li>{{ item.title }} @if(item.userName) { by {{ item.userName }} }</li>
}
</ul>
<button (click)="itemResource.reload()">Reload</button>
} @else if (itemResource.error()) {
<div>Error!!</div>
} @else {
<div>Loading...</div>
}
</section>
Resource のポイントまとめ
- params() の値が変わると loader() が再実行される
- loader は Promise を返す必要がある
- テンプレートでは value(), error(), isLoading() で状態を確認できる
- HTTP 以外の非同期処理にも使える
params を使わないパターン
params を省略すると 値は自動更新されず、reload() のみで再取得になります。
itemResource = resource({
loader: () => firstValueFrom(this.getItems(this.url())),
});
若干ハマりやすいポイントですが、この性質を活用すれば不要な自動更新を抑えることができます。
HttpResource ― HTTP 通信に特化した非同期シグナル
httpResource() は名前の通り HTTP 通信専用の Resource です。
内部で HttpClient をラップしており、以下のような特徴があります。
- メソッド・URL・ヘッダなどをオプションで定義するだけで良い
- parse だけ書けばレスポンスのマッピングができる
- URL のシグナルが変わると自動で再取得
HttpResource の使い方
@Component({
selector: 'app-qiita',
imports: [FormsModule],
templateUrl: './qiita.html',
})
export class Qiita {
constructor(private httpClient: HttpClient) {}
userId = signal('');
url = computed(() => {
return this.userId() === ''
? `https://qiita.com/api/v2/items`
: `https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3A${this.userId()}`;
});
itemHttpResource = httpResource<Item[]>(
() => ({
url: this.url(),
method: 'GET',
headers: { Authorization: 'Bearer ********' },
}),
{
parse: (json: unknown) => {
const rows = json as any[];
return rows.map(row => ({
id: row.id,
title: row.title,
userName: row.user.name,
}));
},
}
);
}
Resource と違い、loader や firstValueFrom を書く必要がありません。
HttpResource のテスト
HttpClient をモックすればテストできます。
resource() でも同様にテストできます。
describe('Qiita', () => {
let component: Qiita;
let fixture: ComponentFixture<Qiita>;
let httpTestingController: HttpTestingController;
const mockItems = [
{
id: 1,
title: 'ほげほげ',
user: {
name: 'user1'
}
},
{
id: 2,
title: 'ふがふが',
user: {
name: 'user2'
}
}
];
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Qiita],
providers: [
provideHttpClient(),
provideHttpClientTesting()
],
})
.compileComponents();
fixture = TestBed.createComponent(Qiita);
component = fixture.componentInstance;
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
it('should load items using httpResource() on initialization', async () => {
fixture.detectChanges();
const requests = httpTestingController.match(
'https://qiita.com/api/v2/items'
);
requests.forEach(req => {
expect(req.request.method).toBe('GET');
req.flush(mockItems);
});
await fixture.whenStable();
fixture.detectChanges();
expect(component.itemHttpResource.hasValue()).toBe(true);
expect(component.itemHttpResource.value()?.length).toBe(2);
});
});
📝 まとめ:Resource と HttpResource の使い分け
両者を触ってみて、次の様な使い分けになるかなと感じました。
🔵 Resource が向くケース
- 既存サービスの HTTP 呼び出しを「シグナル化」したい
- すでに HttpClient を使ったサービス層がある
- HTTP 以外の非同期処理(WebSocket、IndexedDB 等)で使いたい
- 柔軟な前処理・後処理が必要
🟢 HttpResource が向くケース
- 新しく HTTP 通信する機能を作成したい
- 読み取り中心で、処理が単純
- 「URL のシグナルが変わったら自動で再取得したい」
🎤 所感
Resource はとても柔軟で「好きな非同期処理をシグナルにできる強力な仕組み」だと感じました。
一方 HttpResource は HTTP 通信に必要なボイラープレートが大幅に減り、「小さく書きたい」ケースではかなり便利です。
どちらも非同期データを扱う UX を改善してくれるため、
状況に応じてうまく使い分けることで、より直感的でリアクティブなアプリケーションを構築しやすくなると思います。