- Angular
- TypeScript
- 技術 💻
Angular RxJS と Signals の使い分け|toSignal判断フロー
Angular の RxJS と Signals の使い分けは、「時間の流れ(いつ・何回・どの順で起きるか)を扱うなら RxJS、画面に出す“今の値”を持つなら Signal」です。 両者は @angular/core/rxjs-interop の toSignal() / toObservable() でつなげるので、「イベントは RxJS で整えて、最後に signal にして表示する」という組み合わせが基本形になります。
この記事では、Angular 22.2.0 で実際にプロジェクトを作り、検索入力(debounce・switchMap)、HTTP 取得、ポーリング、イベントストリーム、フォーム値、グローバル状態の6ケースを コピペで動くコード で書きました。どれを選ぶかの 判断フロー と ケース別の対応表、toSignal を注入コンテキストの外で呼んだときの NG0203、requireSync で同期値がないときの NG0601 など 実際のエラー文、Vitest と Playwright(Chromium)での 実行結果 もそのまま載せています。
Angular RxJS と Signals の使い分け早見表
| やりたいこと | おすすめ | 使う API | ひとこと理由 |
|---|---|---|---|
| 画面の状態(選択中・件数・フラグ) | Signal | signal / computed |
今の値を持つのが得意 |
| 検索入力を待ってから API を呼ぶ | RxJS → Signal | toObservable → debounceTime / switchMap → toSignal |
時間の制御とキャンセルは RxJS |
| HTTP で1件・一覧を GET | Signal 系リソース | httpResource(v22 で安定版) |
読み込み中・エラー・値を signal で持てる |
| 既存の Observable を返すサービスで取得 | Signal 系リソース | rxResource(v22 で安定版) |
retry など RxJS 演算子もそのまま使える |
| 一定間隔で取得(ポーリング) | RxJS → Signal | timer + switchMap → toSignal |
画面を離れると自動で止まる |
| WebSocket・通知などのイベント列 | RxJS → Signal | scan で状態にまとめて toSignal |
「流れ」を「状態」に変えてから表示 |
| Reactive Forms の値を表示に使う | RxJS → Signal | valueChanges → toSignal(initialValue 必須級) |
最初の値が流れないため |
Observable をそのまま output に |
RxJS | outputFromObservable |
throttleTime などを通した値を親へ |
どうしても subscribe したい |
RxJS | takeUntilDestroyed |
破棄時に自動で購読解除 |
| アプリ全体の状態 | Signal | サービス + signal(大きければ NgRx SignalStore) |
読み取りは同期で簡単 |
| signal の変化を RxJS 演算子で扱う | Signal → RxJS | toObservable |
最初の値は同期、以降は非同期で流れる |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular RxJS と Signals の使い分け|toSignal判断フロー」
迷ったときの判断フロー
「値が1つあればよいか」「時間やキャンセルの制御が要るか」の2点で決まります。 次のフローで上から順に答えていくと、使う API が1つに絞れます。
flowchart TD
A[扱いたいものは?] --> B{画面に出す<br>今の値だけ?}
B -- はい --> S[signal / computed]
B -- いいえ --> C{HTTPで取得する?}
C -- はい --> D{Observableを返す<br>既存サービスがある?}
D -- いいえ --> H[httpResource]
D -- はい --> R[rxResource]
C -- いいえ --> E{待つ・間引く・キャンセル<br>などの時間制御が要る?}
E -- はい --> T[RxJSで整えてから toSignal]
E -- いいえ --> F{イベントが次々に来る?<br>WebSocket・通知など}
F -- はい --> G[scanで状態にして toSignal]
F -- いいえ --> S
ポイントは「最後は signal にして表示する」ことです。 テンプレートでは async パイプの代わりに items() のように読むだけになり、computed で派生値も作れます。逆に「signal の変化を待って何かする(遅らせる・まとめる)」ときだけ toObservable で RxJS 側へ渡します。
検証環境とプロジェクトの作り方
この記事のコードはすべて Angular 22.2.0 / RxJS 7.8.2 / TypeScript 6.0.3 / Node.js 24.21.0 で動かし、テストは Vitest 5.0.2、画面確認は Playwright 1.63.0(Chromium)で行いました。 Angular 22.2 の CLI は Node.js ^22.22.3・^24.15.0・>=26.0.0 のいずれかが必要です。
npx -y -p @angular/cli@22 ng new rxdemo --skip-git --defaults --style=css --ssr=false --routing --ai-config=none
cd rxdemo
学習用のモックAPI(HTTP インターセプター)
サーバーを立てずに試せるよう、/api/* をブラウザ内で返すインターセプターを用意します。 以降のコードはこれを前提にしています。
// src/app/mock-backend.ts
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
export interface Item {
id: number;
name: string;
stock: number;
}
const ITEMS: Item[] = [
{ id: 1, name: 'ボールペン', stock: 120 },
{ id: 2, name: 'ペンケース', stock: 8 },
{ id: 3, name: 'ノート', stock: 45 },
{ id: 4, name: 'ホチキス', stock: 12 },
{ id: 5, name: 'ペン立て', stock: 3 },
];
let statsCount = 0;
/** /api/* をブラウザ内で返すモックAPI(学習用) */
export const mockBackend: HttpInterceptorFn = (req, next) => {
const url = new URL(req.urlWithParams, 'http://localhost');
if (!url.pathname.startsWith('/api/')) return next(req);
let body: unknown;
if (url.pathname === '/api/items') {
const q = url.searchParams.get('q') ?? '';
body = ITEMS.filter((i) => i.name.includes(q));
} else if (url.pathname.startsWith('/api/items/')) {
const id = Number(url.pathname.split('/').pop());
body = ITEMS.find((i) => i.id === id) ?? null;
} else if (url.pathname === '/api/stats') {
body = { checkedAt: ++statsCount, lowStock: ITEMS.filter((i) => i.stock < 10).length };
}
console.log(`[mock] ${req.method} ${req.urlWithParams}`);
return of(new HttpResponse({ status: 200, body })).pipe(delay(200));
};
src/app/app.config.ts の providers には provideHttpClient(withInterceptors([mockBackend])) を追加します(provideHttpClient と withInterceptors は @angular/common/http から読み込み)。
// src/app/item-api.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Item } from './mock-backend';
@Injectable({ providedIn: 'root' })
export class ItemApi {
private readonly http = inject(HttpClient);
search(q: string) {
return this.http.get<Item[]>('/api/items', { params: { q } });
}
get(id: number) {
return this.http.get<Item>(`/api/items/${id}`);
}
stats() {
return this.http.get<{ checkedAt: number; lowStock: number }>('/api/stats');
}
}
rxjs-interop の API 一覧
@angular/core/rxjs-interop が公開している API は7つです(22.2.0 の型定義で確認)。
| API | 向き | 主な用途 | 注入コンテキスト | 安定度(型定義の表記) |
|---|---|---|---|---|
toSignal |
Observable → Signal | 表示用に signal 化 | 必要(injector 指定で回避可) |
publicApi 20.0 |
toObservable |
Signal → Observable | signal の変化に演算子をかける | 必要(injector 指定で回避可) |
publicApi 20.0 |
rxResource |
Observable → Resource | Observable で非同期読み込み | 必要 | publicApi 22.0 |
outputFromObservable |
Observable → output | Observable を output として公開 |
必要(フィールド初期化で使う) | publicApi 19.0 |
outputToObservable |
output → Observable | 子の output を RxJS で扱う |
不要 | publicApi 19.0 |
takeUntilDestroyed |
演算子 | 破棄時に購読を終える | 引数なしなら必要 | publicApi 19.0 |
pendingUntilEvent |
演算子 | SSR で値が出るまで待たせる | 引数なしなら必要 | developerPreview 20.0 |
HTTP 専用の httpResource は @angular/common/http から読み込みます(こちらも型定義で publicApi 22.0)。
検索入力:debounce と switchMap の結果を signal で表示する
入力値は signal、時間の制御は RxJS、表示は signal という分担にします。 toObservable で signal を Observable にし、debounceTime で待ち、switchMap で古いリクエストを取り消してから、toSignal で signal に戻します。
// src/app/demos/item-search.ts
import { Component, inject, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs';
import { ItemApi } from '../item-api';
@Component({
selector: 'app-item-search',
template: `
<input id="q" placeholder="備品を検索" [value]="query()" (input)="query.set($any($event.target).value)" />
<p id="hit">{{ results().length }} 件</p>
<ul>
@for (item of results(); track item.id) {
<li>{{ item.name }}(在庫 {{ item.stock }})</li>
}
</ul>
`,
})
export class ItemSearch {
private readonly api = inject(ItemApi);
/** 入力値は signal で持つ */
readonly query = signal('');
/** 時間の扱い(debounce・キャンセル)は RxJS、表示は signal */
readonly results = toSignal(
toObservable(this.query).pipe(
map((q) => q.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.api.search(q)),
),
{ initialValue: [] },
);
}
initialValue: [] を渡すと型が Signal<Item[]> になり、テンプレートで undefined を気にせず書けます。 渡さない場合の型は Signal<Item[] | undefined> です。
Vitest で「API は1回だけ呼ばれる」を確かめる
Vitest のフェイクタイマーで時間を進め、3回素早く入力しても API 呼び出しが最後の1回だけになることを確認しました。
// src/app/demos/item-search.spec.ts(要点)
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('素早く3回入力しても API は最後の1回だけ呼ばれる', async () => {
const search = vi.fn((q: string) => of([{ id: 1, name: `${q}の結果`, stock: 1 }]));
TestBed.configureTestingModule({ providers: [{ provide: ItemApi, useValue: { search } }] });
const fixture = TestBed.createComponent(ItemSearch);
const cmp = fixture.componentInstance;
TestBed.tick();
for (const q of ['ぺ', 'ペ', 'ペン']) {
cmp.query.set(q);
TestBed.tick(); // toObservable の effect を動かす
await vi.advanceTimersByTimeAsync(100);
}
await vi.advanceTimersByTimeAsync(300);
expect(search.mock.calls.map((c) => c[0])).toEqual(['ペン']);
expect(cmp.results()).toEqual([{ id: 1, name: 'ペンの結果', stock: 1 }]);
});
✓ |rxdemo| src/app/demos/item-search.spec.ts > ItemSearch(debounce + switchMap) > 素早く3回入力しても API は最後の1回だけ呼ばれる 18ms
フェイクタイマー中に await fixture.whenStable() を使うと、Error: Test timed out in 5000ms. で止まりました。 変更検知の予約もタイマーで動くためです。フェイクタイマーを使うテストでは TestBed.tick() で変更検知を進めると安定します。
HTTP 取得:httpResource・rxResource・toSignal の選び方
GET で値を読むだけなら httpResource、Observable を返す既存サービスを使うなら rxResource が短く書けます。 どちらも v22 で安定版になり、value()・isLoading()・error()・status() を signal で持ちます。
// src/app/demos/item-detail.ts
import { Component, inject, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
import { retry } from 'rxjs';
import { ItemApi } from '../item-api';
import { Item } from '../mock-backend';
@Component({
selector: 'app-item-detail',
template: `
<button id="next" (click)="id.update((v) => (v % 5) + 1)">次の備品(id={{ id() }})</button>
<p id="rx">rxResource: {{ rx.isLoading() ? '読み込み中…' : rx.value()?.name }}</p>
<p id="http">httpResource: {{ http.isLoading() ? '読み込み中…' : http.value()?.name }}</p>
`,
})
export class ItemDetail {
private readonly api = inject(ItemApi);
readonly id = signal(1);
/** Observable を返す既存サービスをそのまま使える */
readonly rx = rxResource({
params: () => ({ id: this.id() }),
stream: ({ params }) => this.api.get(params.id).pipe(retry(2)),
});
/** HTTP の GET だけなら httpResource が最短 */
readonly http = httpResource<Item>(() => `/api/items/${this.id()}`);
}
| 書き方 | 向いている場面 | 読み込み中・エラーの状態 | 引数が変わったとき |
|---|---|---|---|
httpResource |
GET で JSON を読むだけ | isLoading() / error() が標準 |
自動で読み直す |
rxResource |
既存の Observable サービス、retry などを足したい |
isLoading() / error() が標準 |
自動で読み直す(前の購読は解除) |
toSignal(http.get()) |
1回だけ読む簡単な表示 | 自分で用意する | 読み直さない |
toSignal(toObservable(x).pipe(switchMap(...))) |
debounce など時間制御が必要 | 自分で用意する | switchMap で切り替え |
params から undefined を返すと、リソースは読み込みをせず idle になります。 「ID が決まるまで取得しない」という条件はこれで書けます。
Vitest で HttpTestingController を使って確かめると、id を1つ変えるたびに rxResource と httpResource がそれぞれ1本ずつリクエストを出し、応答後に両方の value() が新しい備品名になりました。
ポーリング:timer と switchMap を toSignal で表示する
「一定間隔で取得する」は RxJS の timer が得意です。 toSignal はコンポーネントが破棄されると購読を自動で解除するため、画面を離れればポーリングも止まります。
// src/app/demos/stats-poller.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { switchMap, timer } from 'rxjs';
import { ItemApi } from '../item-api';
@Component({
selector: 'app-stats-poller',
template: `<p id="stats">確認 {{ stats()?.checkedAt ?? '-' }} 回目:在庫が少ない備品 {{ stats()?.lowStock ?? '-' }} 件</p>`,
})
export class StatsPoller {
private readonly api = inject(ItemApi);
/** 0秒後に1回、その後2秒ごとに取得。画面を離れると自動で止まる */
readonly stats = toSignal(timer(0, 2000).pipe(switchMap(() => this.api.stats())));
}
前の応答が終わる前に次の周期が来たら、switchMap が前のリクエストを取り消します(全部使うなら concatMap、応答中は飛ばすなら exhaustMap)。
WebSocket・イベントストリーム:scan で状態にまとめてから signal にする
次々に届くイベントは、scan で「最新 N 件の配列」などの状態にしてから toSignal にします。 signal は「今の値」しか持たないため、イベントをそのまま signal にすると、同じ変更検知の間に届いた途中の値は画面に出ません。
// src/app/demos/notice-feed.ts
import { Component, Injectable, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Subject, scan } from 'rxjs';
/** WebSocket などのイベント源の代わり */
@Injectable({ providedIn: 'root' })
export class NoticeBus {
private readonly subject = new Subject<string>();
readonly notices$ = this.subject.asObservable();
push(message: string) {
this.subject.next(message);
}
}
@Component({
selector: 'app-notice-feed',
template: `
<button id="push" (click)="bus.push('入荷: ノート ' + (latest().length + 1))">通知を送る</button>
<ol id="notices">
@for (n of latest(); track $index) { <li>{{ n }}</li> }
</ol>
`,
})
export class NoticeFeed {
protected readonly bus = inject(NoticeBus);
/** 流れてくるイベントを「最新3件の配列」という状態にまとめてから signal にする */
readonly latest = toSignal(
this.bus.notices$.pipe(scan((acc, n) => [n, ...acc].slice(0, 3), [] as string[])),
{ initialValue: [] },
);
}
実際の WebSocket では、rxjs/webSocket の webSocket() が返す Observable を notices$ の位置に置けば同じ形で使えます(検証では Subject で代用)。
フォーム値:valueChanges を toSignal にするときは initialValue を渡す
Reactive Forms の valueChanges は、購読した時点の値を流しません。 そのため initialValue に現在の値を渡すと、最初の表示から正しい値になります。
// src/app/demos/filter-form.ts
import { Component, computed } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-filter-form',
imports: [ReactiveFormsModule],
template: `
<input id="min" type="number" [formControl]="minStock" />
<p id="min-label">{{ label() }}</p>
`,
})
export class FilterForm {
readonly minStock = new FormControl(10, { nonNullable: true });
/** valueChanges は最初の値を流さないので initialValue に現在値を渡す */
readonly min = toSignal(this.minStock.valueChanges, { initialValue: this.minStock.value });
readonly label = computed(() => `在庫 ${this.min()} 個以上を表示`);
}
新しく作るフォームなら、@angular/forms/signals の Signal Forms(v22 の型定義で publicApi 22.0)も選べます。 フォームの値そのものを signal として持てるので、toSignal の橋渡しが要りません。既存の Reactive Forms はこの toSignal の形で少しずつ signal に寄せていけます。
Observable を output にする:outputFromObservable
子コンポーネントのイベントを RxJS で間引いてから親へ渡したいときは outputFromObservable を使います。 購読と解除は Angular が行います。
// src/app/demos/tick-button.ts
import { Component, signal } from '@angular/core';
import { outputFromObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subject, fromEvent, throttleTime, map } from 'rxjs';
@Component({
selector: 'app-tick-button',
template: `<button id="tick" (click)="clicks$.next()">記録する</button> <span id="width">幅 {{ width() }}px</span>`,
})
export class TickButton {
protected readonly clicks$ = new Subject<void>();
/** Observable をそのまま output にする(1秒に1回まで) */
readonly recorded = outputFromObservable(
this.clicks$.pipe(throttleTime(1000), map(() => new Date().toISOString())),
);
readonly width = signal(window.innerWidth);
constructor() {
// subscribe するときは takeUntilDestroyed で後片付けを任せる
fromEvent(window, 'resize')
.pipe(takeUntilDestroyed())
.subscribe(() => this.width.set(window.innerWidth));
}
}
親では <app-tick-button (recorded)="records.update((r) => [...r, $event])" /> のように普通の output と同じく受け取れます。
subscribe するなら takeUntilDestroyed
subscribe は「副作用(ログ送信、外部ライブラリの操作など)」を書くときに使い、takeUntilDestroyed() を必ず付けます。 引数なしで使えるのはコンストラクターやフィールド初期化の中だけです(前の TickButton の例)。ngOnInit などで使うときは inject(DestroyRef) で取得した DestroyRef を takeUntilDestroyed(this.destroyRef) のように渡します。
グローバル状態:サービスに signal を持たせる
アプリ全体で共有する状態は、サービスに signal を持たせ、外には読み取り専用で公開する形が基本です。 読み取りは同期でよく、computed で派生値も作れるため、状態そのものは Signal で持つのが扱いやすくなります。
@Injectable({ providedIn: 'root' })
export class CartState {
private readonly _lines = signal<{ itemId: number; qty: number }[]>([]);
readonly lines = this._lines.asReadonly(); // 外からは読み取り専用
readonly totalQty = computed(() => this._lines().reduce((sum, l) => sum + l.qty, 0));
add(itemId: number) { this._lines.update((lines) => [...lines, { itemId, qty: 1 }]); }
}
状態と操作が増えてきたら NgRx SignalStore に移すと、1か所で整理できます(非同期処理は rxMethod で RxJS の演算子をそのまま使えます)。
toSignal のオプション一覧
| オプション | 既定 | 効果 | 使いどころ |
|---|---|---|---|
initialValue |
なし | 最初の値。型から undefined が消える |
非同期の Observable(HTTP、valueChanges) |
requireSync |
false |
購読した瞬間に値が来ることを保証。型から undefined が消える |
BehaviorSubject、startWith 付き |
manualCleanup |
false |
破棄時に自動で解除しない。Observable の完了まで購読が続く | ルートのサービスなど、アプリと同じ寿命のもの |
injector |
なし | 注入コンテキストの外で作るための Injector | ngOnInit やイベント処理の中で作るとき |
equal |
Object.is |
同じとみなす判定。同じなら更新しない | 中身が同じ新しいオブジェクトが流れてくるとき |
initialValue と requireSync: true は同時に指定しません。 型定義上も、どちらか一方を選ぶ形になっています。
// BehaviorSubject は購読した瞬間に値が来る → Signal<string>(undefined なし)
readonly status = toSignal(this.status$, { requireSync: true });
// ngOnInit などコンテキストの外では、フィールドで取得した Injector を渡す
this.later = toSignal(this.status$, { injector: this.injector, requireSync: true });
よくあるエラーと直し方(実際のエラー文)
Vitest で実際に発生させたエラー文です。 どれも原因がはっきりしているので、文言で見分けられます。
| エラー | 起きる場面 | 直し方 |
|---|---|---|
NG0203: toSignal() can only be used within an injection context ... |
ngOnInit・クリック処理・普通の関数で toSignal を呼んだ |
フィールド初期化かコンストラクターで作る/injector を渡す |
| NG0601: toSignal() called with requireSync but Observable did not emit synchronously. | requireSync: true なのに、HTTP や delay 付きなど非同期の Observable を渡した |
initialValue を使う/startWith を付ける |
| signal を読んだときに元のエラーが投げられる | Observable がエラーで終わった | catchError で値に変える/rxResource の error() を使う |
| テストで値が更新されない | toObservable は2回目以降の値を非同期で流す |
TestBed.tick() で effect を動かす |
NG0203:注入コンテキストの外で toSignal を呼んだ
直す前(ngOnInit の中で呼んでいる)→ 直した後(フィールド初期化で作る、または injector を渡す):
// 直す前
ngOnInit() {
this.items = toSignal(this.items$); // NG0203
}
// 直した後
readonly items = toSignal(this.items$, { initialValue: [] }); // フィールド初期化ならOK
private readonly injector = inject(Injector);
ngOnInit() {
const later = toSignal(this.items$, { injector: this.injector }); // injector を渡せばOK
}
Error: NG0203: toSignal() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://v22.angular.dev/errors/NG0203
NG0601:requireSync なのに同期で値が来ない
toSignal(of(1).pipe(delay(10)), { requireSync: true }) を実行すると NG0601 になりました。非同期の Observable には initialValue を使い、requireSync は BehaviorSubject や startWith(...) 付きのように「購読した瞬間に値が出る」ものだけに使います。
toObservable は最初の値だけ同期で流れる
toObservable は内部で effect を使うため、2回目以降の値は非同期で流れ、途中の値はまとめられます(テストで確認)。
const count = signal(1);
toObservable(count).subscribe((v) => values.push(v)); // 注入コンテキスト内で購読
TestBed.tick();
count.set(2);
count.set(3);
// この時点で values は [1]
TestBed.tick();
// values は [1, 3](2 は飛ばされ、最新の 3 だけ流れる)
「全部の変化を1つずつ処理したい」ときは、signal ではなく Subject などの Observable で持つ のが向いています。
この記事のテスト(検索・リソース・エラー7種)は、Vitest 5.0.2 で Tests 9 passed (9) でした。
Playwright(Chromium)で画面の動きを確認する
ng serve で起動した画面を Playwright で操作し、各ケースが期待どおり動くことを確かめました。 検索欄に「ペン」を1文字ずつ入力し、詳細の「次へ」、2秒以上の待機、通知ボタン2回、フォームへの入力、記録ボタン5回連打、ウィンドウ幅の変更を順に行い、モックAPIのログ([mock] GET ...)から検索リクエストの回数を数えています。
初期: 5 件
「ペン」入力後: 3 件 | items 検索リクエスト: [ '[mock] GET /api/items?q=%E3%83%9A%E3%83%B3' ]
rxResource: ボールペン / httpResource: ボールペン
次へ後: rxResource: ペンケース / httpResource: ペンケース
確認 3 回目:在庫が少ない備品 2 件
通知: 入荷: ノート 2, 入荷: ノート 1
在庫 25 個以上を表示
output(throttle 1秒): 記録 1 件
幅 900px
「ペ」「ン」と2文字入力しても検索リクエストは1回だけで、5回連続クリックしても throttleTime(1000) により記録は1件でした。 検証では Playwright 1.63.0 から、既にインストール済みの Chromium を executablePath で指定して起動しています。
よくある質問
Q. RxJS はもう使わなくてよいのですか?
A. 状態を持つ部分は Signal に置き換えやすくなりましたが、debounce・キャンセル・リトライ・ポーリング・イベントの合成など「時間の扱い」は RxJS が得意です。状態は Signal、時間の流れは RxJS と分担し、toSignal でつなぐのが扱いやすい形です。
Q. async パイプと toSignal はどちらを使えばよいですか?
A. どちらも購読と解除を Angular に任せられます。toSignal にすると computed で派生値を作れて、テンプレートの外(クラスの中)でも同期で値を読めるため、新しく書くコードでは toSignal がまとめやすくなります。
Q. httpResource と rxResource はどう選びますか?
A. GET で JSON を読むだけなら httpResource、HttpClient を包んだ既存のサービスがある、retry などの演算子を足したい、HTTP 以外の Observable を読む、という場合は rxResource です。どちらも v22 で安定版です。
まとめ
- 状態(今の値)は Signal、時間の流れ(待つ・間引く・キャンセル)は RxJS と分担する
- RxJS で整えた結果は
toSignalで signal にして表示し、initialValueでundefinedを消す - HTTP の GET は
httpResource、既存の Observable サービスはrxResource(どちらも v22 で安定版) - イベント列は
scanで状態にしてからtoSignal、フォーム値はinitialValueに現在値を渡す subscribeするときはtakeUntilDestroyed、ngOnInitではDestroyRefを渡す- NG0203 は注入コンテキスト外、NG0601 は
requireSyncの対象違い が原因。フィールド初期化・injector・initialValueで直せる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular RxJS と Signals の使い分け|toSignal判断フロー」
株式会社RJCでは、RxJS と Signals のような新しい書き方も、実際に手を動かして確かめながらチームで取り入れています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。