こんにちは、開発部の鈴木です。

2022年の5月にAngularのモジュール設計と遅延ロードの紹介について紹介しました。
それからAngularのバージョンが上がり、スタンドアロンコンポーネントやSignalsなど新たな機能が追加されています。

また、Angular v19以降ではデフォルトでコンポネート、ディレクティブ、パイプがスタンドアロンに設定されるようになったため、改めて現在のAngularのアプリケーション設計について考え、今までとどう変化するのかを紹介します。

今回紹介するスタンドアロンコンポーネントとSignalsを使用したアプリケーション設計のサンプルコードの全体はGitHubで公開しています。

https://github.com/dddsuzuki/angular-standalone-example

前回の記事で紹介した従来のモジュールを使用したアプリケーション設計のサンプルコードも公開しているので比較対象として参考にしてみてください。

https://github.com/dddsuzuki/angular-module-design-app

モジュールからスタンドアロンコンポーネントへ

今まではアプリケーションの機能や関心事をそれぞれ別のモジュールに分けてアプリケーションを設計するようにしていました。

現在ではコンポーネントとモジュールが一体となったような機能であるスタンドアロンコンポーネントを使用することで、今までよりシンプルに実装できるようになっています。

スタンドアロンコンポーネント

コンポーネントのstandaloneプロパティをtrueに設定することでスタンドアロンコンポーネントを使用することができます。
Angular v19以降ではデフォルトでコンポーネントがスタンドアロンに設定されるようになったため、standaloneプロパティを指定する必要はありません。
なので、今まで通りモジュールでコンポーネントを使用する場合は、standaloneプロパティをfalseに設定しましょう。

スタンドアロンコンポーネントでは、モジュールの変わりにコンポーネントのimportsプロパティで依存関係を管理します。

@Component({
  selector: 'app-todo-list',
  imports: [CommonModule, RouterLink],
  templateUrl: './list.component.html',
})
export class TodoListComponent {
}

また、ルーティングモジュールも作成する必要がなくなり、代わりにRoutesオブジェクトをエクスポートするファイルを作成し、loadChildrenにインポートするだけになりました。

export const routes: Routes = [
  {
    path: 'todo',
    loadChildren: () => import('./todo/todo.routes').then((m) => m.routes),
  },
];

スタンドアロンコンポーネントを使用した設計では、モジュールやルーティングモジュールを使用することは無くなりましたが、今まで通り機能や関心事毎にディレクトリを分け、コンポーネントやサービス、ルーティング設定などをまとめるように設計するという点では今までと変わりありません。

Signalsを使用した状態管理

前回の記事では状態管理の部分を紹介していませんでしたが、今まではサービスでBehaviorSubjectを使用して状態管理を行なっていました。

現在では新たに登場したSignalsを使用して、以下のように状態管理を行うことができようになりました。

@Injectable()
export class TodoDetailService {
  private readonly todoApi = inject(TodoApiService);
  private readonly todoSignal = signal<Todo | null>(null);

  get $todo(): Signal<Todo | null> {
    return this.todoSignal.asReadonly();
  }

  fetch(id: number): void {
    this.todoApi.get(id).subscribe((todo) => {
      this.todoSignal.set(todo);
    });
  }
}

Observableの変数の命名は末尾に$を付ける文化がありましたが、Signalsの変数の場合は先頭に$を付けるのが一般的なようです。

また、テンプレート側ではObservableな値を扱う際に使用していたAsyncPipeも必要なくなり、シンプルに記述できるようになりました。

@let todo = $todo();
@if (todo) {
  <h3>{{ todo.content }}</h3>
  <div>ID: {{ todo.id }}</div>
  <div>内容: {{ todo.content }}</div>
  <div>完了: {{ todo.done ? "✅" : "⏺" }}</div>
}

その他の変化

さて、今回はスタンドアロンコンポーネントとSignalsに主眼を置いてアプリケーション設計の変化について説明しましたが、紹介したサンプルコードの中にはそれ以外にも、テンプレートで使用している@ifなどの制御フロー構文や、inject()関数を使用したDIの方法なども取り入れています。

おわりに

私自身、プロジェクト内で設計や構文にバラ付きが出てしまうことに抵抗があり、既存のプロジェクトを継続して開発している最中は、新しい機能や構文を取り入れることに消極的になってしまっていました。

ですが、ここ数年でAngularに追加された機能や構文はどれも魅力的なものばかりであったので、新規のプロジェクトだけでなく、既存のプロジェクトにも時間を使って取り入れていきたいと思います!