Skip to content

Angular Signalsにおける入力変更時のデータフェッチ方法

問題

入力シグナル(input())をもつAngularコンポーネントで、シグナルの値が変更された際にデータを再取得する必要があります。従来のngOnChangesが非推奨方向にある中、Signalsベースのアプローチでの代替方法が不明です。主な課題は次の通りです:

typescript
@Component()
export class ChartComponent {
    dataSeriesId = input.required<string>();
    fromDate = input.required<Date>();
    toDate = input.required<Date>();
    
    private data = signal<ChartData | null>(null);
}
  • computedは非同期処理をサポートしていない
  • effectは状態変更を意図していない(公式ドキュメントで非推奨)
  • 入力シグナルの変更を検知しdataシグナルを更新する適切な方法が必要

最新の解決策: Angular 19のrxResource (推奨)

Angular 19で導入されたrxResourceが最適解です。自動的なキャンセル・ローディング状態処理に対応:

typescript
import { rxResource } from "@angular/core/rxjs-interop";

@Component({
  template: `
    {{ resource.value() }}
    @if(resource.isLoading()) { 読み込み中... } 
    @if(resource.error(); as error) { {{ error }} }
  `,
})
export class ChartComponent {
  dataSeriesId = input.required<string>();
  fromDate = input.required<Date>();
  toDate = input.required<Date>();

  // 入力値を集約したシグナル
  params = computed(() => ({
    id: this.dataSeriesId(),
    from: this.fromDate(),
    to: this.toDate()
  }));

  // rxResourceでデータ取得を宣言
  resource = rxResource({
    request: this.params,
    loader: (loaderParams) => this.fetchData(loaderParams.request)
  });

  private fetchData({ id, from, to }: { 
    id: string; from: Date; to: Date 
  }) {
    return this.http.get<ChartData>(`/api/data`, { 
      params: { id, from: from.toISOString(), to: to.toISOString() }
    });
  }
}

特徴

  • 自動キャンセル: 新しいリクエスト時に前回リクエストを自動中止
  • 状態管理: value(), isLoading(), error() でUI状態を管理
  • 依存追跡: 入力シグナルの変更を自動検知
  • Zoneless対応: 変更検知パフォーマンスが最適化

実装上の注意

HttpClientを利用した場合、再リクエスト時に進行中のHTTPリクエストは自動的にキャンセルされます
動作例: StackBlitz デモ


Angular 19未満での代替案: toObservabletoSignal

v19以前ではrxjs-interopの変換メソッドを利用:

typescript
import { toObservable, toSignal } from "@angular/core/rxjs-interop";

@Component()
export class ChartComponent {
  // 入力シグナル定義
  dataSeriesId = input.required<string>();
  fromDate = input.required<Date>();
  toDate = input.required<Date>();

  // 入力値を集約
  params = computed(() => ({
    id: this.dataSeriesId(),
    from: this.fromDate(),
    to: this.toDate()
  }));

  // Observable ⇆ Signal変換
  data = toSignal(
    toObservable(this.params).pipe(
      switchMap(params => this.fetchData(params))
    )
  );

  private fetchData({ id, from, to }) {
    return this.http.get(`/api/data/${id}`, { 
      params: { from, to } 
    });
  }
}

メリット

  • 自動リクエスト発行: 入力値変更を検知し自動実行
  • キャンセル処理: switchMapで前リクエストを破棄
  • シンプルなインターフェース: テンプレート側はdata()のみ呼出

重要

switchMapの代わりにmergeMapを使用すると、リクエスト順序保証されない可能性があります
キャンセル不要な場合はmergeMap、キャンセル必要ならswitchMapを選択


非推奨アプローチ: effectを使った方法

公式ドキュメントで推奨されていませんが、allowSignalWritesフラグで実装可能:

typescript
export class ChartComponent {
  // ...入力シグナル
  private data = signal<ChartData | null>(null);
  private destroyRef = inject(DestroyRef);

  constructor() {
    let subscription: Subscription;
    
    effect(() => {
      subscription?.unsubscribe();
      
      // Signal直接参照で依存関係追跡
      const params = {
        id: this.dataSeriesId(),
        from: this.fromDate(),
        to: this.toDate()
      };
      
      subscription = this.fetchData(params).subscribe(data => {
        this.data.set(data); // 状態更新
      });
    }, { allowSignalWrites: true }); // 状態変更許可フラグ
    
    // コンポーネント破棄時に購読解除
    this.destroyRef.onDestroy(() => subscription.unsubscribe());
  }
}

問題点

  • 手動購読解除: コンポーネントライフサイクルに対応必要
  • キャンセル漏れリスク: 新リクエスト時の旧リクエストキャンセルが手動管理
  • エッジケース: 並行リクエスト時の状態競合リスク

回避すること

チームプロジェクトでの不整合リスクが高く、メモリリーク要因となるため特殊なケース以外での使用は避けてください
代替手段が利用できない場合に限り、徹底したテストが必須


パフォーマンスとベストプラクティス

  1. 不要な再取得防止

    typescript
    // 無効なパラメータ時はスキップ
    params = computed(() => {
      if (!this.dataSeriesId()) return null;
      return { id: this.dataSeriesId(), from: this.fromDate(), to: this.toDate() };
    });
  2. デバウンス処理追加

    typescript
    // 頻繁な変更を抑制 (300msディレイ)
    data = toSignal(
      toObservable(this.params).pipe(
        debounceTime(300),
        distinctUntilChanged(),
        switchMap(/* ... */)
      )
    );
  3. エラー処理の標準化

    typescript
    resource = rxResource({
      request: this.params,
      loader: (p) => this.fetchData(p).pipe(
        catchError(err => of({
          error: err.message,
          lastValid: this.resource.value() // 旧値を保持
        }))
      )
    });

各手法の比較表

特徴rxResource (v19+)toObservable変換effect利用
自動リクエストキャンセル○ (switchMap使用時)△ (手動管理)
ローディング状態管理××
エラー処理仕組み×
メモリリークリスク
コードの簡潔さ
Angularサポート公式推奨互換性対応非推奨

結論

  1. Angular v19以上
    rxResourceを積極採用
    (最新機能 + 最小コード + 自動管理)

  2. Angular v16-v18
    toObservable + toSignal + switchMap が現実解
    (安定動作 + リクエストキャンセル対応)

  3. どうしても必要な特殊ケース
    effectallowSignalWrites使用(テスト必須)

最新プロジェクトの立ち上げではAngular 19+の採用とrxResourceの活用が、生産性・メンテナンス性の観点で最適解となります。