🔥

Angular SignalsのEffectは即時関数と使うとわかりやすい

2024/07/02に公開

Angular SignalsのEffectは、Effect内で利用しているSignalsが変化してる度に実行される便利な機能です。

effect(() => {
  console.log('signal variable is changed to ' + signalVariable());
})

しかし、こうしてSignalが最初に呼び出されている時は問題なく実行されるのですが、非同期や条件文の中では呼び出しがスキップされます。

effect(() => {
  // this.countが変化する度に実行される
  this.insideSimpleEffect = this.count();
});
effect(async () => {
  // 実行されない
  await new Promise<void>((resolve) => resolve());
  this.insideAwaitRequestAnimationFrame = this.count();
});
effect(() => {
  // 実行されない
  requestAnimationFrame(() => {
    this.insideRequestAnimationFrame = this.count();
  });
});
effect(() => {
  // 実行されない
  setTimeout(() => {
    this.insideSetTimeout = this.count();
  }, 100);
});
effect(() => {
  // 実行されない
  if (this.insideSimpleEffect === 5) {
    this.insideCondition = this.count();
  }
});

https://stackblitz.com/edit/stackblitz-starters-unvngn?file=src%2Fmain.ts で実際に呼び出されないことを確認できます。

原因

Signalが実際に読み込まれた時だけ、Effectはその値に対する依存関係を登録するためです。そのため、条件文に隠されたり、非同期処理のように異なるスタックフレームで作業が行われると、エフェクトは依存関係を登録できないためにSignalの値が変わっても変更は検知されません。

Source: https://github.com/angular/angular/issues/56773#issuecomment-2198479276

対応

1. 必ず最初の行で代入を行う

依存関係を登録するために、必ず利用するSignalを最初の行で代入を行います。一番シンプル。 ただ書く人の意識次第なので、見逃しやすいです。

ts
  effect(() => {
+   const count = this.count();
    if (this.insideSimpleEffect === 5) {
-     this.insideCondition = this.count();
+     this.insideCondition = count;
    }
  });

2. 即時関数を使う

Effectでは即時関数を呼び出す形にして、引数にSignalを渡すことで、依存関係を登録することができます。個人的にはこちらの方が好きで、何のためのEffectかが明確になります。

effect(() => ((count: number) => {
  if (this.insideSimpleEffect === 5) {
    this.insideCondition = count;
  }
})(this.count()));

まとめ

Signalsはとても使い勝手がよく、特にFormの変更検知ではとても便利です。ただ、この仕組みをわかっていないと不具合の温床になるので、ルール化して使っていくことがおすすめです。

それではまた。

Discussion