はじめに
v16 から Angular に Signal が導入されました。
シンプルな記述でリアクティブな値を管理でき、非常に便利です。
また、Signal を使った変更検知は、従来の zone.js によるものと異なり、コンポーネントツリー全体を確認する必要がないため、パフォーマンスの向上も期待できます。
しかし、使い勝手が良い反面、注意が必要な現象に遭遇し沼ったため、備忘録として残します。
Computed Signal とは
Computed Signal とは、Signal から派生した値を持つ読み取り専用の Signal です。
以下のように、computed
関数を使って定義します。
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
Computed Signal が更新されないケース
Computed Signal を定義時点で、computed
関数に渡す Signal を評価できないと、更新を追跡できないようです。
配列やオブジェクトのイテレーションメソッドに渡すコールバック関数で Signal を評価する場合、そういったことが起こる可能性があります。
以下のように、初期値がundefined
の配列undefinedNumbers
のイテレーションメソッド内では、定義時点で this.count()
を評価できません。
結果、後からundefinedNumbers
に値を代入しても、count
の更新を追跡できず、undefinedComputedSignal
がundefined
のままになってしまうようです。
import { Component, computed, signal } from "@angular/core";
import { bootstrapApplication } from "@angular/platform-browser";
@Component({
selector: "app-root",
template: `
<div>undefined computed signal: {{ undefinedComputedSignal() }}</div>
<button (click)="countUp()">count up</button>
`,
})
export class App {
count = signal(0);
undefinedNumbers: number[];
undefinedComputedSignal = computed(() =>
this.undefinedNumbers?.find((number) => number === this.count())
);
countUp() {
this.count.update((count) => count + 1);
this.undefinedNumbers = [5, 10, 15];
}
}
bootstrapApplication(App);
回避策
以下のように、イテレーションメソッドのコールバック関数外で参照を作ってあげると、count
の更新を追跡してくれます。
- undefinedComputedSignal = computed(() =>
- this.undefinedNumbers?.find((number) => number === this.count())
- );
+ undefinedComputedSignal2 = computed(() => {
+ const count = this.count();
+ return this.undefinedNumbers?.find((number) => number === count);
+ });
import { Component, computed, signal } from "@angular/core";
import { bootstrapApplication } from "@angular/platform-browser";
@Component({
selector: "app-root",
template: `
<div>undefined computed signal2: {{ undefinedComputedSignal2() }}</div>
<button (click)="countUp()">count up</button>
`,
})
export class App {
count = signal(0);
undefinedNumbers: number[];
undefinedComputedSignal2 = computed(() => {
const count = this.count();
return this.undefinedNumbers?.find((number) => number === count);
});
countUp() {
this.count.update((count) => count + 1);
this.undefinedNumbers = [5, 10, 15];
}
}
bootstrapApplication(App);
StackBlitz
検証のために遊んだ StackBlitz を貼っておきます。
https://stackblitz.com/edit/stackblitz-starters-57skebhe?file=src%2Fmain.ts
おわりに
Computed Signal は定義時に評価した Signal の更新を追跡するようです。
紹介したケースのように、undefined
, []
, {}
のイテレーションメソッド内に追跡したい Signal を書くと、この不具合が発生する可能性があるため、注意が必要です。
最後に、本記事の内容は、あくまで実験の結果であって、Signalの挙動を保証するものではない点にはご注意ください。
参考
- Angular Signals | Angular
https://angular.dev/guide/signals - Angular Signals の状態管理!実装パターンと RxJS の相互運用。
https://ugo.tokyo/angular-signals/