この章では、コンポーネントの責務を整理し、小さくシンプルなコンポーネントに分割するステップを段階的に学びます。コンポーネントは Angular アプリケーションのもっとも基本的な構成要素です。Angular アプリケーションをどのようにリファクタリングすればよいかを学ぶためには、まずはコンポーネントのリファクタリングから始めるべきでしょう。
サンプルアプリケーションの作成
ここからはサンプルアプリケーションを段階的にリファクタリングしながら、その目的やコツを学びます。まずはそのサンプルアプリケーションを作成しましょう。Angular CLI を使ってローカル開発する場合は、ng new
コマンドを使ってプロジェクトを作成してください。StackBlitz のテンプレートを使ってもかまいません。
今回作成するのはユーザー情報を管理するシンプルなアプリケーションです。求められる機能は、https://reqres.in/の ユーザー API[1]で取得したユーザーを一覧して表示することだけです。最初の実装では、すべての処理を AppComponent
だけでおこないます。サービスへの分割も、コンポーネントの分割もまだ何もおこなっていません。次の 3 つのファイルを用意しましょう。
export interface User {
id: string;
first_name: string;
last_name: string;
}
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;
});
}
}
<ul>
<li *ngFor="let user of users">
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
次の画像のようにユーザーリストが表示される状態を用意します。
ここを出発地点として、よりメンテナンスしやすいアプリケーションとなるようコンポーネントを分割していきましょう。
コンポーネントの責任
あらためて、現在の AppComponent
のソースコードを見てみましょう。
<ul>
<li *ngFor="let user of users">
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}
</li>
</ul>
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 つに注目します。
- アプリケーションブートストラップに使われるルートコンポーネントとなること
- ユーザーの配列を API から取得すること
- ユーザーの配列をリストとして表示すること
- ユーザーの情報を表示すること
これは 単一責任原則[2] に反しており、このままでは、AppComponent
はいくつもの理由で頻繁に変更されることになります。変更される理由が多いソースコードは、加えられたたくさんの変更の意図が混ざり合い、不明確になっていきます。どのような意図で書かれているのかわかりにくくなると、そのソースコードを変更することが難しくなっていきます。問題が見つかっても簡単に修正できません。そのため、AppComponent
の責任はできるだけ少なくしなければなりません。
Angular アプリケーションとして機能しなければならない以上、AppComponent
がルートコンポーネントとしての役割を捨てることはできません。よって、それ以外の責任を別のコンポーネントへと委譲すること、つまりコンポーネントの分割が必要です。
UserListItemComponent
の分割
最初に切り出すのは「ユーザーの情報を表示する」という責任です。ユーザーリストの繰り返し単位のコンポーネントを UserListItemComponent
として分割しましょう。このコンポーネントの責任は、渡された 1 件のユーザーをリストアイテムとして表示することです。それ以外の理由でこのコンポーネントが変更されてはいけません。
コンポーネントの分割は次のような手順で行います。
- 新しいコンポーネントのファイルを作成する(
ng g component --standalone
) - 元のコンポーネントのテンプレートから新しいコンポーネントに分割したい範囲を移植する
- 新しいコンポーネントに必要なプロパティやメソッドを定義する
- 元のコンポーネントのテンプレートを新しいコンポーネントで置き換える
UserListItemComponent
のために切り出されるテンプレートは次の範囲です。UserListItemComponent
のクラスはこのテンプレートに必要な user
プロパティを持ち、親コンポーネントからインプットを通じてデータを受け取らなければいけません。
#{{ user.id }} {{ user.first_name }} {{ user.last_name }}
結果として、UserListItemComponent
は次のようになります。
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
が完成したら、AppComponent
の imports
配列に UserListItemComponent
を追加します。これで AppComponent
のテンプレートから <user-list-item>
タグを呼び出すことができます。
最後に、AppComponent
のテンプレートの一部をUserListItemComponent
で置き換えるとコンポーネントの分割は完了です。これでAppComponent
はユーザー情報の表示に関する責任から開放されました。たとえば、今後の機能拡張によってユーザーのメールアドレスやアイコン画像を表示することになっても、そのために AppComponent
を変更する必要はありません。
<ul>
<li *ngFor="let user of users">
<user-list-item [user]="user"></user-list-item>
</li>
</ul>
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>
タグを使うために、@Component
の imports
配列に UserListItemComponent
を追加することを忘れないでください。
<ul>
<li *ngFor="let user of users">
<user-list-item [user]="user"></user-list-item>
</li>
</ul>
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
が完成したら、AppComponent
の imports
配列にインポートし、テンプレートで <user-list>
タグを呼び出します。もう <user-list-item>
タグは直接呼び出さなくなったので、UserListItemComponent
をインポートする必要はありません。結果として、AppComponent
の実装は次のようになります。
<user-list [users]="users"></user-list>
@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 における一般的なパターンとして、次のようにまとめることができます。
- 親コンポーネントが持つテンプレートの一部を切り出す
- 切り出したテンプレートを新しいコンポーネントに移動し、そのテンプレートに必要なデータをインプットプロパティとして定義する
- 親コンポーネントのテンプレートから切り出したコンポーネントを呼び出し、データを渡す
この分割によって何が起きているかというと、「データを表示する」というひとまとまりだった責任を分割し、データをどのように管理するかに責任を持つ親コンポーネントと、渡されたデータをどのように表示するかに責任を持つ子コンポーネントに分割しているということです。
サンプルアプリケーションで登場した UserListComponent
や UserListItemComponent
は、インプットプロパティで渡されたデータだけに依存し、それをテンプレートに表示することに特化しています。このようなコンポーネントは プレゼンテーショナルコンポーネント(presentational component)と呼ばれることもあります。その名のとおり、データのプレゼンテーション(表示)だけを責任とするということです。
表示だけに責任を持つプレゼンテーションコンポーネントに対し、データをプレゼンテーションコンポーネントへ渡す役割を持つコンポーネントを コンテナコンポーネント (container component)と呼びます。コンテナコンポーネントは、プレゼンテーショナルコンポーネントへ渡すデータの取得や保持などに責任を持ちますが、それがどのように表示されるかについては関与しません。このようにコンポーネントが持つ関心を 2 種類に分類するパターンは、コンテナ/プレゼンテーションパターン[3] として知られています。
コンテナ/プレゼンテーションパターンが常にあらゆるコンポーネントのリファクタリングに有効であるとは限りません。ですが、複雑なコンポーネントを前にして、どうにか責任を分割して少しでもシンプルにしたいという場合には、まずはじめに試みるべきパターンです。
この章のまとめ
- ひとつのコンポーネントが持つ責任が大きくなったときには、コンポーネントの分割が必要です。
- コンポーネントの親子関係を作ることで、責任を分割する方法を学びました。
- コンポーネントを分割する典型的なパターンとして、コンテナ/プレゼンテーションパターンを学びました。