はじめに
タイトルのとおり 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 の実行結果( レスポンス )を一部加工して、そのままメソッドの戻り値としているのに対し、修正後では toSignal
で Signal に変換して返却しています。
これにより、呼び出しもとでは 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);
});
}
この次に続く PUT
や DELETE
も同じなのですが、これらの処理はクリックイベントから実行されます。( 補足-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 で扱うにあたり、次の記事を参考にさせていただきました。
大変助かりました。 深く感謝申し上げます。
-
Angular Signals
-
HttpClient を Signal で扱うために参考にしたページ
-
エラー解決にあたり参考にしたページ