1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Angular] HttpClient で取得したデータを Signal で扱う際にやったこと

Posted at

はじめに

タイトルのとおり HttpClient で取得したデータを Angular Signal で扱ってみる試みです。
HttpClient では Observable を返します が、それを Signal で扱うにはどうすればよいか、がこの記事の主眼です。

なお本記事で扱うコードにはエラー処理を施しておりません。
あらかじめご承知おきください。

環境

今回の記事の内容は次の環境で実施したものです。

環境 バージョン 備考
Angular CLI v17.3.6 ng version で確認
Angular v17.3.7 同上
TypeScript v5.4.5 同上
zone.js v0.14.5 同上
Node.js v18.19.0 同上
npm v10.5.1 同上

修正したコードと役割

修正したコードは service.ts, component.ts, compoonent.html です。
それぞれの役割は次のとおりです。

  • service.ts
    • HttpClient を使って API を実行
    • API の実行結果( レスポンス )をコール元に返却する
  • component.ts
    • service 経由で API から返却されたレスポンスを html テンプレートで参照する変数にセットする
  • compoonent.html
    • component.ts でセットされた変数を参照して描画する

本記事の構成

本記事では service.ts, component.ts, compoonent.html の修正前後のコードを挙げて差分を見ていきます。

修正前後のコード比較(service.ts)

GET

修正前

  public get$(): Observable<MessageModel[]> {
    return this.http.get<HttpResponseBodyModel>(this.host + '/message/get', this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );
  }

修正後

  public get$(): Signal<MessageModel[] | undefined> {
    const observable = this.http.get<HttpResponseBodyModel>(this.host + '/message/get', this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );

    return toSignal(observable);
  }

大きな違いは Observable を返却するか Signal を返却するかです。
修正前では HttpClient の実行結果( レスポンス )を一部加工して、そのままメソッドの戻り値としているのに対し、修正後では toSignalSignal に変換して返却しています。

これにより、呼び出しもとでは GET で得た値を Signal で扱うことができるようになりました。

以降の POST, PUT, DELETE についても基本的な修正内容は同じですので、それぞれの項目での説明は省きます。

POST

修正前

  public register$(body: MessageModel): Observable<MessageModel[]> {
    return this.http.post(this.host + '/message/post', body, this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );
  }

修正後

  public register$(body: MessageModel): Signal<MessageModel[] | undefined> {
    const observable = this.http.post(this.host + '/message/post', body, this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );

    return toSignal(observable);
  }

PUT

修正前

  public update$(body: MessageModel): Observable<MessageModel[]> {
    return this.http.put(this.host + '/message/put', body, this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );
  }

修正後

  public update$(body: MessageModel): Signal<MessageModel[] | undefined> {
    const observable = this.http.put(this.host + '/message/put', body, this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );

    return toSignal(observable);
  }

DELETE

修正前

  public delete$(body: MessageModel): Observable<MessageModel[]> {
    this.httpOptions.body = body;
    return this.http.delete(this.host + '/message/delete', this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );
  }

修正後

  public delete2$(body: MessageModel): Signal<MessageModel[] | undefined> {
    this.httpOptions.body = body;
    const observable = this.http.delete(this.host + '/message/delete', this.httpOptions).pipe(
      map((response) => {
        const res: any = response;
        const resBody: HttpResponseBodyModel = res.body;
        return resBody.messages;
      })
    );

    return toSignal(observable);
  }

修正前後のコード比較(component.ts)

共通

修正後のコードだけにある処理

export class HttpClientVerificationComponent implements OnInit {
  injector = inject(EnvironmentInjector);
  // ...(略)...
}

こちらは後述の 補足-2 で触れている runInInjectionContext で使用するために必要な処理を追加しています。

GET

修正前後で差分なし

  public messageInfoList$ = this.httpClientService.get$();

GET における差分はありません。 Observable を扱っていたときと同じコードで、そのまま Signal が返却されるコードにも対応できました。

POST

修正前

  public onClickRegister(event: any): void {
    // ..(略)..
    this.messageInfoList$ = this.httpClientService.register$(body);
  }

修正後

  public onClickRegister(event: any): void {
    // ..(略)..
    runInInjectionContext(this.injector, () => {
      this.messageInfoList$ = this.httpClientService.register$(body);
    });
  }

この次に続く PUTDELETE も同じなのですが、これらの処理はクリックイベントから実行されます。( 補足-1 )
そのためか 修正前のコード では NG0203 が発生しました。 この修正は当該エラーに対応するためのものです。
詳しくは 補足-2 をご参照ください。

なお PUT, DELETE についても基本的な修正内容は同じですので、それぞれの項目での説明は省きます。

PUT

修正前

  public onClickUpdate(event: any): void {
    // ..(略)..
    this.messageInfoList$ = this.httpClientService.update$(body);
  }

修正後

  public onClickUpdate(event: any): void {
    // ..(略)..
    runInInjectionContext(this.injector, () => {
      this.messageInfoList$ = this.httpClientService.update$(body);
    });
  }

DELETE

修正前

  public onClickDelete(event: any): void {
    // ..(略)..
    this.messageInfoList$ = this.httpClientService.delete$(body);
  }

修正後

  public onClickDelete(event: any): void {
    // ..(略)..
    runInInjectionContext(this.injector, () => {
      this.messageInfoList$ = this.httpClientService.delete$(body);
    });
  }

補足-1

GET 以外の HTTP メソッドは UI からのボタンクリックイベントで実行されます。

補足-2

修正前後のコード比較(service.ts) に挙げたコードのとおり、本記事の POST, PUT, DELETE では、 API 実行後のレスポンスを Signal で返却しています。
そして、それを利用している component 側では返却されたレスポンスを messageInfoList$ にセットします。
( messageInfoList$GET 時も参照している変数です )

messageInfoList$ へ値を設定するにあたり GET と同じように修正前後で差分なし、つまり修正なしで対応できるかと思ったのですが、実行時に次のエラーが発生しました。

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`.

どうやら toSignal をクリックイベントで扱うことは NG のようです。
このエラーに対応するために POST, PUT, DELETE の API を実行する際に次の修正を施しました。
( 下記は POST の例 )

 public onClickRegister(event: any): void {
   runInInjectionContext(this.injector, () => {
     this.messageInfoList$ = this.httpClientService.register$(body);
   });
 }

修正の際は こちら を参考にしました。

修正前後のコード比較(component.html)

修正前

    @for ((messageInfo of messageInfoList$ | async); track messageInfo; let i = $index) {
      <tr>
        <td>{{messageInfo.id}}</td>
        <td>{{messageInfo.message}}</td>
      </tr>
    }

修正後

    <!-- messageInfoList$ を参照するさいに `()` を付与している点に注目 -->
    @for ((messageInfo of messageInfoList$()); track messageInfo; let i = $index) {
      <tr>
        <td>{{messageInfo.id}}</td>
        <td>{{messageInfo.message}}</td>
      </tr>
    }

手前味噌で恐縮ですが、 こちら で触れておりますとおり、 Signal に対して参照する際は () を付与しないとエラーになります。コンパイルも通りません。
以下は () を付与しなかったときのエラー内容です。

Type 'Signal<MessageModel[] | undefined>' must have a '[Symbol.iterator]()' method that returns an iterator.ngtsc(2488)
http-client-verification.component.ts(9, 44): Error occurs in the template of component HttpClientVerificationComponent.

まとめにかえて

以上、 HttpClient で取得したデータを Signal で扱う試みでした。
キモとなるポイントは以下になるかと思います。

  • toSginal での変換
  • クリックイベントで toSignal を扱うと NG0203 が発生する
  • その対応のために runInInjectionContext でラップする
  • Signal を html テンプレートで参照する際は () を付与してメソッド呼び出しとする

対応方法が分かってしまえば ( 修正差分をご覧のとおり ) 大きな修正は必要とせず、 Signal に変換可能であることが分かりました。

ソースコード

修正差分やソースコードは以下からご覧になれます。
ご興味あればどうぞご確認ください。

差分( PR )

リポジトリ内のコード

参考

HttpClient を Signal で扱うにあたり、次の記事を参考にさせていただきました。
大変助かりました。 深く感謝申し上げます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?