跳到主要内容

スタンドアロンコンポーネントとモジュール

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組み込み・サードパーティ・自作)をインポート
providersDIコンテナに登録するサービス(現在は@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)
HttpClientModuleprovideHttpClient()
BrowserAnimationsModuleprovideAnimationsAsync()
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.tsbootstrapApplicationapp/layout.tsx
ルーティング設定app.routes.ts(コード定義)app/ディレクトリ構造(ファイルベース)
設定ファイルapp.config.tsnext.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() { /* ... */ }
}