Chapter 04

1-1.コンポーネントの分割

lacolaco
lacolaco
2024.10.23に更新

この章では、コンポーネントの責務を整理し、小さくシンプルなコンポーネントに分割するステップを段階的に学びます。コンポーネントは Angular アプリケーションのもっとも基本的な構成要素です。Angular アプリケーションをどのようにリファクタリングすればよいかを学ぶためには、まずはコンポーネントのリファクタリングから始めるべきでしょう。

サンプルアプリケーションの作成

ここからはサンプルアプリケーションを段階的にリファクタリングしながら、その目的やコツを学びます。まずはそのサンプルアプリケーションを作成しましょう。Angular CLI を使ってローカル開発する場合は、ng newコマンドを使ってプロジェクトを作成してください。StackBlitz のテンプレートを使ってもかまいません。

今回作成するのはユーザー情報を管理するシンプルなアプリケーションです。求められる機能は、https://reqres.in/の ユーザー API[1]で取得したユーザーを一覧して表示することだけです。最初の実装では、すべての処理を AppComponent だけでおこないます。サービスへの分割も、コンポーネントの分割もまだ何もおこなっていません。次の 3 つのファイルを用意しましょう。

app/user.ts
export interface User {
  id: string;
  first_name: string;
  last_name: string;
}
app/app.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { User } from './user';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  private readonly http = inject(HttpClient);

  users: User[] = [];

  ngOnInit() {
    this.http
      .get<{ data: User[] }>('https://reqres.in/api/users')
      .subscribe((resp) => {
        this.users = resp.data;
      });
  }
}
app/app.component.html
<ul>
  <li *ngFor="let user of users">
    #{{ user.id }} {{ user.first_name }} {{ user.last_name }}
  </li>
</ul>

次の画像のようにユーザーリストが表示される状態を用意します。

ユーザーリストの表示

ここを出発地点として、よりメンテナンスしやすいアプリケーションとなるようコンポーネントを分割していきましょう。

コンポーネントの責任

あらためて、現在の AppComponent のソースコードを見てみましょう。

app.component.html
<ul>
  <li *ngFor="let user of users">
    #{{ user.id }} {{ user.first_name }} {{ user.last_name }}
  </li>
</ul>
app.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { User } from './user';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  private readonly http = inject(HttpClient);

  users: User[] = [];

  ngOnInit() {
    this.http
      .get<{ data: User[] }>('https://reqres.in/api/users')
      .subscribe((resp) => {
        this.users = resp.data;
      });
  }
}

アプリケーションを構成するコンポーネントは AppComponent しかないため、必然的に多くの役割が AppComponent に含まれています。ここでは特に次の 4 つに注目します。

  1. アプリケーションブートストラップに使われるルートコンポーネントとなること
  2. ユーザーの配列を API から取得すること
  3. ユーザーの配列をリストとして表示すること
  4. ユーザーの情報を表示すること

これは 単一責任原則[2] に反しており、このままでは、AppComponentはいくつもの理由で頻繁に変更されることになります。変更される理由が多いソースコードは、加えられたたくさんの変更の意図が混ざり合い、不明確になっていきます。どのような意図で書かれているのかわかりにくくなると、そのソースコードを変更することが難しくなっていきます。問題が見つかっても簡単に修正できません。そのため、AppComponent の責任はできるだけ少なくしなければなりません。

Angular アプリケーションとして機能しなければならない以上、AppComponent がルートコンポーネントとしての役割を捨てることはできません。よって、それ以外の責任を別のコンポーネントへと委譲すること、つまりコンポーネントの分割が必要です。

UserListItemComponent の分割

最初に切り出すのは「ユーザーの情報を表示する」という責任です。ユーザーリストの繰り返し単位のコンポーネントを UserListItemComponent として分割しましょう。このコンポーネントの責任は、渡された 1 件のユーザーをリストアイテムとして表示することです。それ以外の理由でこのコンポーネントが変更されてはいけません。

コンポーネントの分割は次のような手順で行います。

  1. 新しいコンポーネントのファイルを作成する(ng g component --standalone
  2. 元のコンポーネントのテンプレートから新しいコンポーネントに分割したい範囲を移植する
  3. 新しいコンポーネントに必要なプロパティやメソッドを定義する
  4. 元のコンポーネントのテンプレートを新しいコンポーネントで置き換える

UserListItemComponent のために切り出されるテンプレートは次の範囲です。UserListItemComponentのクラスはこのテンプレートに必要な user プロパティを持ち、親コンポーネントからインプットを通じてデータを受け取らなければいけません。

user-list-item.component.html
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}

結果として、UserListItemComponent は次のようになります。

user-list-item.component.ts
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { User } from '../user';

@Component({
  selector: 'user-list-item',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user-list-item.component.html',
})
export class UserListItemComponent {
  @Input()
  user!: User;
}

UserListItemComponent が完成したら、AppComponentimports 配列に UserListItemComponent を追加します。これで AppComponent のテンプレートから <user-list-item> タグを呼び出すことができます。

最後に、AppComponentのテンプレートの一部をUserListItemComponentで置き換えるとコンポーネントの分割は完了です。これでAppComponentはユーザー情報の表示に関する責任から開放されました。たとえば、今後の機能拡張によってユーザーのメールアドレスやアイコン画像を表示することになっても、そのために AppComponentを変更する必要はありません。

app.component.html
<ul>
  <li *ngFor="let user of users">
    <user-list-item [user]="user"></user-list-item>
  </li>
</ul>
app.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { UserListItemComponent } from './user-list-item/user-list-item.component';
import { User } from './user';

@Component({
  selector: 'my-app',
  standalone: true,
  // UserListItemComponent をインポートする
  imports: [CommonModule, UserListItemComponent],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  private readonly http = inject(HttpClient);

  users: User[] = [];

  ngOnInit() {
    this.http
      .get<{ data: User[] }>('https://reqres.in/api/users')
      .subscribe((resp) => {
        this.users = resp.data;
      });
  }
}

UserListComponentの分割

続いて、「ユーザーの配列をリストとして表示する」という役割をAppComponentから切り出すことにしましょう。先ほどと同じように、新しく UserListComponent を作成し、テンプレートを切り出します。このコンポーネントにはユーザーの配列を受け取るための users インプットプロパティが必要です。また、UserListComponent のテンプレートで <user-list-item> タグを使うために、@Componentimports 配列に UserListItemComponent を追加することを忘れないでください。

user-list.component.html
<ul>
  <li *ngFor="let user of users">
    <user-list-item [user]="user"></user-list-item>
  </li>
</ul>
user-list.component.ts
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { User } from '../user';
import { UserListItemComponent } from '../user-list-item/user-list-item.component';

@Component({
  selector: 'user-list',
  standalone: true,
  imports: [CommonModule, UserListItemComponent],
  templateUrl: './user-list.component.html',
})
export class UserListComponent {
  @Input()
  users!: User[];
}

UserListComponentが完成したら、AppComponentimports 配列にインポートし、テンプレートで <user-list> タグを呼び出します。もう <user-list-item> タグは直接呼び出さなくなったので、UserListItemComponent をインポートする必要はありません。結果として、AppComponentの実装は次のようになります。

app.component.html
<user-list [users]="users"></user-list>
app.component.ts
@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, UserListComponent],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  private readonly http = inject(HttpClient);

  users: User[] = [];

  ngOnInit() {
    this.http
      .get<{ data: User[] }>('https://reqres.in/api/users')
      .subscribe((resp) => {
        this.users = resp.data;
      });
  }
}

このようにコンポーネントを分割することで、AppComponentが持つ責任は、アプリケーションのルートコンポーネントであることと、ユーザーリストのデータを取得することになりました。まだ残りがありますが、それは次の章で取り扱うことにしましょう。

コンポーネント分割のパターン

この章でおこなったコンポーネント分割の流れは Angular における一般的なパターンとして、次のようにまとめることができます。

  1. 親コンポーネントが持つテンプレートの一部を切り出す
  2. 切り出したテンプレートを新しいコンポーネントに移動し、そのテンプレートに必要なデータをインプットプロパティとして定義する
  3. 親コンポーネントのテンプレートから切り出したコンポーネントを呼び出し、データを渡す

この分割によって何が起きているかというと、「データを表示する」というひとまとまりだった責任を分割し、データをどのように管理するかに責任を持つ親コンポーネントと、渡されたデータをどのように表示するかに責任を持つ子コンポーネントに分割しているということです。

サンプルアプリケーションで登場した UserListComponentUserListItemComponent は、インプットプロパティで渡されたデータだけに依存し、それをテンプレートに表示することに特化しています。このようなコンポーネントは プレゼンテーショナルコンポーネント(presentational component)と呼ばれることもあります。その名のとおり、データのプレゼンテーション(表示)だけを責任とするということです。

表示だけに責任を持つプレゼンテーションコンポーネントに対し、データをプレゼンテーションコンポーネントへ渡す役割を持つコンポーネントを コンテナコンポーネント (container component)と呼びます。コンテナコンポーネントは、プレゼンテーショナルコンポーネントへ渡すデータの取得や保持などに責任を持ちますが、それがどのように表示されるかについては関与しません。このようにコンポーネントが持つ関心を 2 種類に分類するパターンは、コンテナ/プレゼンテーションパターン[3] として知られています。

コンテナ/プレゼンテーションパターンが常にあらゆるコンポーネントのリファクタリングに有効であるとは限りません。ですが、複雑なコンポーネントを前にして、どうにか責任を分割して少しでもシンプルにしたいという場合には、まずはじめに試みるべきパターンです。

この章のまとめ

  • ひとつのコンポーネントが持つ責任が大きくなったときには、コンポーネントの分割が必要です。
  • コンポーネントの親子関係を作ることで、責任を分割する方法を学びました。
  • コンポーネントを分割する典型的なパターンとして、コンテナ/プレゼンテーションパターンを学びました。

参考リンク

脚注
  1. https://reqres.in/api/users ↩︎

  2. https://プログラマが知るべき97のこと.com/エッセイ/単一責任原則/ ↩︎

  3. https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 ↩︎