# Angular 2026のRxJS:演算子・Subject・Signals相互運用
> Angular 2026のRxJS:本番で使われる演算子、Subject、Signalsとの相互運用パターンを、面接で最も問われるポイントとあわせて習得します。
- Published: 2026-07-02
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: angular, rxjs, signals, observables, typescript, deep-dive
- Reading time: 10 min
---
AngularにおけるRxJSは、Signalsがコンポーネントの同期的な状態追跡のあり方を刷新した2026年になっても、非同期プログラミングの屋台骨であり続けています。Observableに手を伸ばすべき場面、Signalのほうが適している場面、そしてその2つがどのように相互運用されるかを把握することは、いまやすべてのAngular開発者に求められる中核的なスキルです。本稿では、面接で自信を持って答えられる回答と、行き詰まってしまう回答とを分ける演算子・Subject・相互運用パターンを詳しく掘り下げます。
> **SignalsとObservablesを一言で**
>
> Signalsは常に現在値を持つ同期的な状態をモデル化し、Observablesは時間の経過とともに発生するイベントの非同期ストリームをモデル化します。Angular 20は両方を維持しており、`@angular/core/rxjs-interop`パッケージが両者を公式につなぐ橋渡しの役割を担います。
## AngularのSignalsと並んでRxJSがいまなお重要な理由
Signalsの登場を受けて、多くのチームがRxJSはもう不要になるのではないかと問い始めました。メジャーバージョンを3つ重ねた現在の答えは「いいえ」です。Signalsは、いま値を持っている状態、たとえばカウンター、フォームのフィールド、選択中のタブなどを表現するのに秀でています。一方Observablesは、時間の経過とともに到着し、変換・キャンセル・合成を必要としうるイベント、たとえばHTTPレスポンス、WebSocketメッセージ、キーボード入力、ルーターイベント、タイマーなどをモデル化するのに秀でています。
Angular自身のAPIがこの棲み分けを可視化しています。`HttpClient`は依然としてObservableを返します。リアクティブフォームは`valueChanges`と`statusChanges`をObservableとして公開します。ルーターイベントはObservableを通じてストリーミングされます。複数の非同期イベントが同じチャネルを流れるあらゆる場所で、RxJSの演算子は依然として最も表現力の高い手段であり続けています。公式の[Angularリアクティビティガイド](https://angular.dev/guide/signals/rxjs-interop)は、この2つのモデルを競合ではなく補完し合う関係として扱っており、[RxJSのドキュメント](https://rxjs.dev/guide/overview)も、少しでも複雑なデータフローには欠かせない参考資料であり続けています。
コードレビューで通用する実践的な指針はこうです。テンプレートで読み取るコンポーネントのローカルな状態にはSignalsを、演算子を必要とする非同期パイプラインにはObservablesを使う。相互運用レイヤーが両者を接続するため、どちらのモデルも壁になることはありません。
合成演算子はこの点をさらに裏づけます。複数の非同期ソースの協調、たとえばルートパラメータとユーザーの保存済みフィルタ、そしてリアルタイムの価格フィードをマージするといった処理こそ、`combineLatest`、`withLatestFrom`、`forkJoin`が設計上想定していたものです。Signalsは他のsignalから算出値を導けますが、「3つのソースすべてが少なくとも1つの値を発行したあとにのみ発行する」といった時間軸に基づく協調は表現できません。時間の経過にわたる宣言的な合成、これがRxJSがツールボックスに残り続ける普遍的な理由です。
## すべてのAngular開発者が頼る中核的なRxJS演算子
演算子は、Observableを受け取って新しいObservableを返す純粋関数です。定番の面接シナリオは入力に応じた逐次検索(search-as-you-type)です。というのも、入力の間引き、重複のスキップ、古いリクエストのキャンセル、エラーからの復帰という4つの演算子を一度に扱うからです。
```typescript
// search.component.ts
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-search',
imports: [ReactiveFormsModule],
template: ``,
})
export class SearchComponent {
private http = inject(HttpClient);
query = new FormControl('');
results$ = this.query.valueChanges.pipe(
debounceTime(300), // wait until typing pauses for 300ms
distinctUntilChanged(), // ignore emissions that repeat the last value
switchMap((term) => // cancel the in-flight request, start a fresh one
this.http.get(`/api/search?q=${term}`).pipe(
catchError(() => of([])) // recover from a failed request without killing the stream
)
)
);
}
```
面接官が探りを入れてくる微妙なポイントは、なぜ`switchMap`が外側のレベルにあり、`catchError`がその内側にネストされているのかという点です。内側のHTTP Observableに`catchError`を置くと、失敗したリクエストは空配列を返し、外側のストリームは流れ続けます。`catchError`を外側のパイプに移すと、最初のエラーで検索全体が終了してしまい、以降のすべてのキーストロークが機能しなくなります。4つの平坦化演算子はそれぞれ異なる並行性の問いに答えるものであり、誤った演算子を選ぶことはAngularアプリで最も多い競合状態の原因の1つです。
> **平坦化演算子の選び方**
>
> `switchMap`は直前の内側リクエストをキャンセルし、最新のものだけを残します(タイプアヘッド検索)。`mergeMap`はすべての内側リクエストを並行実行します(独立した並列アップロード)。`concatMap`はリクエストを厳密な順序でキューに並べます(重なってはならない逐次書き込み)。`exhaustMap`は現在の処理が終わるまで新しい入力を無視します(二重クリックされた送信ボタンの抑止)。
この選択を正しく行うことが、不安定な機能を予測可能な機能へと変えます。[SharpSkillのRxJS演算子モジュール](/technologies/angular/interview-questions/rxjs-operators)は、各演算子が防ぐ競合状態そのものを再現する段階別の設問で、これらの違いを徹底的に訓練します。
## Subjectとマルチキャスト:BehaviorSubject、ReplaySubject、Subject
素のObservableはユニキャストで、サブスクライバーごとに新しい実行がトリガーされます。SubjectはObservableでもありObserverでもあるため、マルチキャストになります。この性質により、Subjectはサービス内に軽量な状態ストアやイベントバスを構築する標準的な手段となっています。
3つのバリアントは、遅れて購読したサブスクライバーに何をリプレイするかで異なります。素の`Subject`は過去の値を一切発行しないため、サブスクライバーは購読後に到着した値だけを見ます。`BehaviorSubject`は現在値を保持して即座にリプレイするため、初期値が必要です。`ReplaySubject`は設定可能な数の過去の発行をバッファリングし、それらすべてをリプレイします。
```typescript
// cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
// BehaviorSubject holds the latest value and replays it to new subscribers
private readonly itemsSubject = new BehaviorSubject([]);
// expose a read-only stream, never the writable Subject itself
readonly items$ = this.itemsSubject.asObservable();
add(item: string): void {
const current = this.itemsSubject.value; // synchronous access to the last value
this.itemsSubject.next([...current, item]); // emit the next immutable state
}
}
```
ここでは2つの習慣が重要です。第一に、Subjectそのものではなく`asObservable()`を公開することで、外部コードが`next()`を呼び出して裏口から状態を変更するのを防ぎます。第二に、その場で変更するのではなく新しい配列参照を発行することで、変更検知と等価性チェックを予測可能に保ちます。このパターンはSignalsより前から存在し、複数の無関係なコンポーネントが消費する共有・注入可能な状態にはいまも十分に機能します。
とはいえ、テンプレートで読み取る単一の現在値を保持するためだけに使われる`BehaviorSubject`は、いまや書き込み可能なSignalとして表現したほうがよい場合が多く、そうすれば購読と`asObservable()`という定型処理が不要になります。ストアが発行値の変換・デバウンス・他ストリームとの合成も必要とする場合には、これらの演算子にSignalの直接的な等価物が存在しないため、Subjectのほうが依然として強力な選択肢です。あらゆる場面で一方をデフォルトにするのではなく、ユースケースごとに両者を使い分けられることが、面接官が評価する習熟度の証です。
## toSignalとtoObservableによるRxJSとSignalsの相互運用
相互運用パッケージは2つの主役級の関数を提供します。`toSignal`はObservableを、テンプレートが直接消費できる読み取り専用のSignalに変換し、購読のライフサイクルを自動的に管理します。`toObservable`はその逆で、SignalをObservableに変換し、RxJSの演算子がその変化を処理できるようにします。
```typescript
// dashboard.component.ts
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';
@Component({ selector: 'app-dashboard', template: `{{ user()?.name }}` })
export class DashboardComponent {
private http = inject(HttpClient);
// a Signal drives the query; toObservable turns each change into a stream event
readonly userId = signal(1);
private readonly user$ = toObservable(this.userId).pipe(
switchMap((id) => this.http.get<{ name: string }>(`/api/users/${id}`))
);
// toSignal subscribes immediately and unsubscribes on destroy, no manual teardown
readonly user = toSignal(this.user$, { initialValue: null });
}
```
`initialValue`オプションは注目に値します。これがないと、`toSignal`はまだ何も発行されていないため、値または`undefined`という型のSignalを返します。`initialValue: null`を渡すと、テンプレートには明確な初期状態とよりきれいな型が与えられます。`BehaviorSubject`のような同期的なソースでは、`requireSync: true`によって最初の読み取り時に値が利用可能であることが保証され、型から`undefined`が完全に取り除かれます。この双方向の橋渡しこそが、既存のObservableパイプラインをすべて書き換えることなく、コードベースが段階的にSignalsを採用することを可能にします。関連する[Angular Signals面接モジュール](/technologies/angular/interview-questions/angular-signals)はリアクティビティモデルを深く扱い、[Angular 18のSignals機能概要](/blog/angular/angular-18-signals-new-features)はこれらのプリミティブがどう進化してきたかを追っています。
## rxResourceでObservableをsignalリソースへ橋渡しする
Angular 20は、Observableからの非同期データをsignalベースの状態へ読み込む宣言的な手段として`rxResource`を推奨しています。読み込み中・エラー・解決済みの状態をsignalとして追跡するため、HTTP呼び出しがかつて必要としていた手作業の購読管理の大半を取り除きます。
```typescript
// products.component.ts
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({ selector: 'app-products', template: `` })
export class ProductsComponent {
private http = inject(HttpClient);
readonly category = signal('laptops');
// Angular 20 renamed the fields: request -> params and loader -> stream
readonly products = rxResource({
params: () => ({ category: this.category() }),
stream: ({ params }) =>
this.http.get(`/api/products?category=${params.category}`),
});
// products.value(), products.isLoading() and products.error() are all signals
}
```
Angular 19の`request`/`loader`からAngular 20の`params`/`stream`へのリネームはよくある落とし穴なので、現行のフィールド名を挙げられることは面接で最新の知識を持っている証になります。`category` signalが変化すると、`rxResource`は直前のリクエストをキャンセルして新しいストリームを開始し、`switchMap`のセマンティクスを無償で提供します。[rxResource APIリファレンス](https://v20.angular.dev/api/core/rxjs-interop/rxResource)は状態のライフサイクル全体を文書化しています。
## takeUntilDestroyedでメモリリークを防ぐ
コンポーネントより長く生き残る手作業の`subscribe()`はすべてメモリをリークし、破棄されたビューに対してコールバックを発火させる可能性があります。従来の解決策は、`ngOnDestroy`で完了させるSubjectを用いた`takeUntil(this.destroy$)`パターンでした。Angularはこの定型処理を、購読を現在のインジェクションコンテキストまたは明示的な`DestroyRef`に結びつける演算子`takeUntilDestroyed`で置き換えました。
```typescript
// live-feed.component.ts
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ selector: 'app-live-feed', template: `` })
export class LiveFeedComponent {
private destroyRef = inject(DestroyRef);
constructor() {
interval(1000)
// completes the subscription automatically when the component is destroyed
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tick) => console.log('tick', tick));
}
}
```
コンストラクタまたはフィールド初期化子の内部で呼び出された場合、`takeUntilDestroyed()`は周囲の`DestroyRef`を読み取るため引数を必要としません。それ以外の場所、たとえばメソッドの内部で呼び出された場合は、上記のように明示的な`DestroyRef`を渡す必要があります。`toSignal`や`rxResource`を優先すれば手作業の購読を完全に回避でき、これがリークを避ける最もきれいな方法です。素の購読が本当に必要な場合には、`takeUntilDestroyed`が正しいガードとなります。
> **インジェクションコンテキストの落とし穴**
>
> インジェクションコンテキストの外で引数なしに`takeUntilDestroyed()`を呼び出すと、実行時エラーを投げます。イベントハンドラ、`setTimeout`、あるいはRxJSのコールバックの内部での購読はそのコンテキストの外にあるため、`DestroyRef`は(通常は注入されたフィールドとして)より早い段階で捕捉し、明示的に渡さなければなりません。これを忘れることが、「takeUntilDestroyed() can only be used within an injection context」というエラーの最も頻繁な原因です。
## 練習しておく価値のあるAngular RxJS面接質問
面接官は同じ高シグナルの話題を繰り返し取り上げがちです。これらに端的に答えられることが、RxJSを実際に使ってきた候補者と、読んだだけの候補者とを分けます。
- **ホットObservableとコールドObservable。** コールドObservableは購読ごとにプロデューサーを開始するため、各サブスクライバーは独立した実行(新しいHTTPリクエスト)を得ます。ホットObservableは1つのプロデューサーをサブスクライバー間で共有するもので、Subjectや`share()`が生み出すのがこれです。
- **switchMap対mergeMap対concatMap対exhaustMap。** `switchMap`は直前の内側Observableをキャンセルします(検索に最適)。`mergeMap`はすべての内側Observableを並行実行します。`concatMap`はそれらを順序どおりキューに並べます。`exhaustMap`は1つが実行中の間、新しい入力を無視します(フォームの二重送信の防止に最適)。
- **メモリリークをどう防ぐか。** `toSignal`、`rxResource`、または`async`パイプを優先しましょう。いずれも自動的に購読解除します。手作業の購読には`takeUntilDestroyed`を使います。
- **ObservableをSignalに変換すべきタイミング。** テンプレートが値を同期的に必要とし、かつソースが非同期である場合には`toSignal`に手を伸ばし、`async`パイプなしで変更検知が最新値を読み取れるようにします。
時間のプレッシャーの中でこれらの段階別バージョンに取り組むことが、身につける最速の方法です。[Angularテクノロジーハブ](/technologies/angular)は、これらの質問に直接対応するRxJS・Signals・変更検知の各モジュールをまとめています。
## まとめ
RxJSとSignalsは、現代のAngularスタックにおいてライバルではなくパートナーです。次のプロジェクトや面接に持ち込む価値のある要点は次のとおりです。
- 同期的なコンポーネントの状態にはSignalsを、演算子を必要とする非同期イベントパイプラインにはObservablesを使う。
- 古い処理をキャンセルするには`switchMap`、重複を抑止するには`exhaustMap`に手を伸ばし、`catchError`は内側のObservableに置いて外側のストリームが失敗を生き延びるようにする。
- Subjectの状態は`asObservable()`を通じて公開し、イミュータブルな値を発行して変更検知を予測可能に保つ。
- 2つのモデルは`toSignal`と`toObservable`で橋渡しし、きれいな型のために`initialValue`または`requireSync`を渡す。
- Angular 20では`rxResource`で非同期データを宣言的に読み込み、`params`と`stream`というフィールド名を忘れない。
- 残りのすべての手作業の購読は`takeUntilDestroyed`でガードし、リークを排除する。
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ja/blog/angular/rxjs-angular-operators-subjects-signals-interop