スタンドアロンコンポーネントとモジュール
Angular 14で導入されたスタンドアロンコンポーネントにより、従来のNgModuleを使わない開発が可能になりました。モダンAngularの推奨アプローチを解説します。
1. NgModule(従来のアプローチ)
ReactやNext.jsにはNgModuleに相当するモジュールシステムの概念がありません。AngularのNgModuleは、Angular 2〜13までの主要な構成単位でした。
@NgModule デコレータ
NgModuleは@NgModuleデコレータを使って定義します。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserService } from './user.service';
@NgModule({
declarations: [
AppComponent, // このモジュールが所有するコンポーネント・ディレクティブ・パイプ
UserListComponent,
],
imports: [
BrowserModule, // 他のモジュールをインポート
FormsModule,
AppRoutingModule,
],
providers: [
UserService, // 依存性注入のプロバイダ(推奨: @Injectable({ providedIn: 'root' }))
],
exports: [
UserListComponent, // 他のモジュールで使用可能にするコンポーネント
],
bootstrap: [AppComponent], // ルートモジュールのみ:起動コンポーネント
})
export class AppModule {}
各プロパティの役割
| プロパティ | 役割 |
|---|---|
declarations | このモジュールが所有するコンポーネント・ディレクティブ・パイプを登録 |
imports | 外部モジュール(Angular組み込み・サードパーティ・自作)をインポート |
providers | DIコンテナに登録するサービス(現在は@Injectable({ providedIn: 'root' })推奨) |
exports | 他のモジュールから使用可能にするコンポーネント・ディレクティブ・パイプ |
bootstrap | ルートモジュールのみ指定:アプリ起動時のルートコンポーネント |
NgModuleが必要だった理由
従来のAngularでは、コンポーネントを使用するには必ずどこかのNgModuleにdeclarationsとして登録する必要がありました。NgModuleはコンパイルコンテキストの境界として機能し、どのコンポーネント・ディレクティブ・パイプが利用可能かを定義していました。
// Angular 13以前:UserCardComponent を使うには AppModule の declarations に追加が必要
@NgModule({
declarations: [AppComponent, UserCardComponent], // ここに全コンポーネントを列挙
...
})
export class AppModule {}
2. スタンドアロンコンポーネント(推奨)
Angular 14で導入され、Angular 17以降のデフォルトとなったスタンドアロンコンポーネントは、NgModuleなしで動作します。
standalone: true の指定
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [
CommonModule, // *ngIf, *ngFor などのディレクティブ
RouterLink, // ルーターリンクディレクティブ
UserCardComponent, // 他のスタンドアロンコンポーネントを直接インポート
],
template: `
<div *ngFor="let user of users">
<app-user-card [user]="user" />
</div>
<a routerLink="/home">ホームへ</a>
`,
})
export class UserListComponent {
users = [{ name: '山田太郎' }, { name: '鈴木花子' }];
}
NgModuleとスタンドアロンの比較
| 比較項目 | NgModule | スタンドアロン |
|---|---|---|
| コンポーネント登録 | declarations配列が必要 | 不要 |
| 依存のインポート | モジュールレベルで管理 | コンポーネントレベルで管理 |
| コードの見通し | モジュールファイルを別途確認が必要 | コンポーネントファイル1つで完結 |
| Tree shaking | モジュール単位 | コンポーネント単位(より細かい) |
| React/Next.jsとの比較 | Reactにない概念 | Reactのimport文に類似 |
ng new --standalone でのプロジェクト生成
Angular 17以降では、ng newコマンドでデフォルトにスタンドアロンプロジェクトが生成されます。
# Angular 17以降:デフォルトでスタンドアロン
ng new my-app
# 明示的にスタンドアロンを指定
ng new my-app --standalone
# 従来のNgModuleベースで生成(非推奨)
ng new my-app --no-standalone
生成されるファイルの違い:
// スタンドアロン(新しい): app.component.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {}
// NgModuleベース(従来): app.module.ts が存在
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
bootstrap: [AppComponent],
})
export class AppModule {}
3. bootstrapApplication
スタンドアロンコンポーネントでは、AppModuleの代わりにbootstrapApplication関数でアプリを起動します。
main.ts でのアプリケーション起動
// main.ts(スタンドアロン方式)
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
],
};
関数型プロバイダ
モダンAngularでは、モジュールの代わりに関数型プロバイダを使用します。
| 従来(NgModuleベース) | モダン(関数型プロバイダ) |
|---|---|
RouterModule.forRoot(routes) | provideRouter(routes) |
HttpClientModule | provideHttpClient() |
BrowserAnimationsModule | provideAnimationsAsync() |
StoreModule.forRoot(reducers) | provideStore(reducers) |
EffectsModule.forRoot(effects) | provideEffects(effects) |
// app.config.ts(詳細な設定例)
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([authInterceptor])),
provideAnimationsAsync(),
],
};
4. NgModule → スタンドアロンへの移行
既存のNgModuleベースのプロジェクトをスタンドアロンに移行する手順を解説します。
Angular CLI による自動移行
Angular CLIには自動移行コマンドが用意されています。
# 自動移行スキーマを実行
ng generate @angular/core:standalone
# 移行モードの選択画面が表示される:
# 1. convert-to-standalone: コンポーネントをスタンドアロンに変換
# 2. prune-ng-modules: 不要になったNgModuleを削除
# 3. standalone-bootstrap: bootstrapApplicationに変換
手動移行の手順
ステップ1: コンポーネントに standalone: true を追加
// Before(NgModuleベース)
@Component({
selector: 'app-user-card',
template: `<div>{{ user.name }}</div>`,
})
export class UserCardComponent {}
// After(スタンドアロン)
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
template: `<div>{{ user.name }}</div>`,
})
export class UserCardComponent {}
ステップ2: NgModuleのdeclarationsからコンポーネントを削除しimportsへ移動
@NgModule({
declarations: [
AppComponent,
// UserCardComponent を削除(スタンドアロンになったため)
],
imports: [
BrowserModule,
UserCardComponent, // importsに移動
],
})
export class AppModule {}
ステップ3: 依存関係の整理
// スタンドアロンコンポーネントが他のスタンドアロンコンポーネントをimport
@Component({
standalone: true,
imports: [
CommonModule,
UserCardComponent, // スタンドアロンコンポーネントを直接import
SharedButtonComponent,
],
})
export class UserListComponent {}
ハイブリッドアプローチ
移行期間中は、NgModuleとスタンドアロンコンポーネントを共存させることができます。
// NgModule内でスタンドアロンコンポーネントをimport可能
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StandaloneButtonComponent, // スタンドアロンコンポーネントをimportsに追加
],
})
export class AppModule {}
// スタンドアロンコンポーネント内でNgModuleをimport可能
@Component({
standalone: true,
imports: [
LegacyFeatureModule, // 従来のNgModuleをimport可能
],
})
export class NewFeatureComponent {}
5. プロジェクト構成のベストプラクティス
Feature-based ディレクトリ構成
スタンドアロンコンポーネントを使ったモダンAngularの推奨プロジェクト構成:
src/
├── app/
│ ├── core/ # シングルトンサービス・ガード・インターセプター
│ │ ├── guards/
│ │ │ └── auth.guard.ts
│ │ ├── interceptors/
│ │ │ └── auth.interceptor.ts
│ │ └── services/
│ │ └── auth.service.ts
│ ├── shared/ # 共有コンポーネント・ディレクティブ・パイプ
│ │ ├── components/
│ │ │ └── button/
│ │ │ ├── button.component.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── features/ # 機能ごとのディレクトリ
│ │ ├── dashboard/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ │ └── dashboard.component.ts
│ │ │ ├── services/
│ │ │ └── index.ts
│ │ └── user/
│ │ ├── components/
│ │ │ └── user-card.component.ts
│ │ ├── pages/
│ │ │ └── user-list.component.ts
│ │ ├── services/
│ │ │ └── user.service.ts
│ │ └── index.ts
│ ├── app.component.ts
│ ├── app.config.ts # bootstrapApplication の設定
│ └── app.routes.ts # ルート定義
└── main.ts
Next.js の app/ ディレクトリ構成との比較
| 比較項目 | Angular(スタンドアロン) | Next.js |
|---|---|---|
| エントリポイント | main.ts → bootstrapApplication | app/layout.tsx |
| ルーティング設定 | app.routes.ts(コード定義) | app/ディレクトリ構造(ファイルベース) |
| 設定ファイル | app.config.ts | next.config.js |
| 機能分割 | features/ディレクトリ | app/内のディレクトリ |
| 共有コンポーネント | shared/ディレクトリ | components/ディレクトリ |
共有コンポーネントの管理
// shared/components/button/button.component.ts
@Component({
selector: 'app-button',
standalone: true,
template: `
<button [class]="variant" (click)="clicked.emit()">
<ng-content />
</button>
`,
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' = 'primary';
@Output() clicked = new EventEmitter<void>();
}
Barrel Exports(index.ts)
Barrel exportにより、インポートパスを簡潔に保てます。
// shared/index.ts
export { ButtonComponent } from './components/button/button.component';
export { InputComponent } from './components/input/input.component';
export { ModalComponent } from './components/modal/modal.component';
// ❌ Barrel exportなし(パスが長い)
import { ButtonComponent } from '../../../shared/components/button/button.component';
import { InputComponent } from '../../../shared/components/input/input.component';
// ✅ Barrel exportあり(シンプル)
import { ButtonComponent, InputComponent } from '../../../shared';
// features/user/pages/user-list.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent, InputComponent } from '../../../shared';
import { UserCardComponent } from '../components/user-card.component';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, ButtonComponent, InputComponent, UserCardComponent],
template: `
<div class="user-list">
<app-input placeholder="検索..." (valueChange)="search($event)" />
<app-user-card *ngFor="let user of users" [user]="user" />
<app-button variant="primary" (clicked)="loadMore()">さらに読み込む</app-button>
</div>
`,
})
export class UserListComponent {
users = this.userService.getUsers();
constructor(private userService: UserService) {}
search(query: string) { /* ... */ }
loadMore() { /* ... */ }
}