Angular Signalsとコンポーネント間通信 を読んで Signals を使ったコンポーネントの実装パターンを学んだ。component が複雑になるとロジックや状態管理を service に分離したくなる。ここで Angular Signals を使ったときの component と service の分離について考えてみた。
signals の導入
まずは signals を使うところから始める。ベースは Tour of Heroes の dashboard.component.ts を使う。参考までに最初のコードを示すとおおよそ次のようになっている。
@Component({ selector: 'app-dashboard', template: ` <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </div> <app-hero-search></app-hero-search>`, styleUrls: [ './dashboard.component.css' ] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }
heroes を signal 化すると次のようになる。
@Component({ selector: 'app-dashboard', template: ` <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of $heroes()" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </div> <app-hero-search></app-hero-search>`, styleUrls: ['./dashboard.component.css'], }) export class DashboardComponent implements OnInit { $heroes = signal<Hero[]>([]); constructor(private heroService: HeroService) {} ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService .getHeroes() .subscribe((heroes) => this.$heroes.set(heroes.slice(1, 5))); } }
signal の更新に set()
を使い、値の読み取りは getter を使うようになるのが主な変更点である。
このくらいのコード量ならこれで困ることもないが、コードが増えていくと仮定して hero の取得ロジックと取得したデータの管理を別の class に分けることにする。
service への切り出し
service は DashboardComponent 用の service として DashboardService という名前で作成する。
interface DashboardState { heroes: Hero[]; } @Injectable() class DashboardService { private readonly heroService = inject(HeroService); $state = signal<DashboardState>({ heroes: [] }); getHeroes(): void { this.heroService .getHeroes() .subscribe((heroes) => this.$state.set({ heroes: heroes.slice(1, 5) })); } }
HeroService との依存はこの DashboardService に閉じ込めるようにし component が意識しない形にする。もし DashboardComponent で loading の状態が必要になったときは DashboardState にプロパティを追加すればよい。 この service を component から参照すると component は次のようになる。
@Component({ selector: 'app-dashboard', template: ` <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of $heroes()" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </div> <app-hero-search></app-hero-search>`, styleUrls: ['./dashboard.component.css'], providers: [DashboardService], }) export class DashboardComponent implements OnInit { private readonly service = inject(DashboardService); $heroes = computed(() => this.service.$state().heroes); ngOnInit(): void { this.service.getHeroes(); } }
$heroes
は computed()
を使うことで service で管理している signal から派生した signal になり WritableSignal ではなく readonly で扱うことができる。つまり state の更新の管理も service に閉じ込められる。
なんとなく component と service の役割は分けられたように思える。
まとめ
Angular Signals を自分で触ったことがなかったので、お試しで素振りしてみた。 API 自体も開発者プレビューでまだどうなるかわからないが、最初の一歩としての備忘録とする。