とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

Angular Signals を使ったときの component と service の分離

Angular Signalsとコンポーネント間通信 を読んで Signals を使ったコンポーネントの実装パターンを学んだ。component が複雑になるとロジックや状態管理を service に分離したくなる。ここで Angular Signals を使ったときの component と service の分離について考えてみた。

signals の導入

まずは signals を使うところから始める。ベースは Tour of Heroesdashboard.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();
  }
}

$heroescomputed() を使うことで service で管理している signal から派生した signal になり WritableSignal ではなく readonly で扱うことができる。つまり state の更新の管理も service に閉じ込められる。

なんとなく component と service の役割は分けられたように思える。

まとめ

Angular Signals を自分で触ったことがなかったので、お試しで素振りしてみた。 API 自体も開発者プレビューでまだどうなるかわからないが、最初の一歩としての備忘録とする。