Angular スタンドアロンコンポーネント:Angular 22 移行ガイドとベストプラクティス

Angular アプリケーションを NgModule からスタンドアロンコンポーネントへ移行するための完全ガイドです。公式 CLI の3ステップ移行、セレクターレスコンポーネント、OnPush デフォルト、Angular 22 での loadComponent による遅延読み込みを解説します。

NgModule からスタンドアロンコンポーネントツリーへの移行を示す Angular スタンドアロンコンポーネントの移行アーキテクチャ

Angular のスタンドアロンコンポーネントは、NgModule の必要性を排除し、ボイラープレートを削減するとともに、アプリケーション全体にわたるきめ細かい遅延読み込みを実現します。Angular 19 でスタンドアロンがデフォルトとなり、Angular 22 でセレクターレスコンポーネントが安定版となった今、レガシーなモジュールベースのコードベースを移行することは、シンプルかつ効果の大きい取り組みとなっています。

重要ポイント

公式の Angular CLI スキーマティックは、3回のパスで移行の大部分を自動的に処理します。一般的なエンタープライズアプリケーションであれば、1スプリントで変換を完了でき、コンポーネント単位の遅延読み込みによってバンドルサイズが30〜50%削減されます。

NgModule とスタンドアロンコンポーネントの違い

NgModule は Angular 2 以来、コンポーネントのコンパイルコンテキストとして機能してきました。すべてのコンポーネント、ディレクティブ、パイプは、必ず1つのモジュールに宣言する必要があり、共有機能を利用するには、モジュールのインポートとエクスポートを慎重に管理する必要がありました。これにより、無関係な機能間に密結合が生まれ、ツリーシェイキングが困難になっていました。

スタンドアロンコンポーネントはこのモデルを覆します。各コンポーネントが @Component デコレータの imports 配列で自身の依存関係を直接宣言します。モジュールへの登録も、共有モジュールも、アプリケーション全体のバレルエクスポートも不要になります。Angular 22 では、セレクターレスコンポーネントにより、文字列セレクターなしで直接インポートできるようになり、このパターンがさらに簡素化されました。

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: \`
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  \`
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

imports 配列が NgModule の依存関係グラフ全体を置き換えます。バンドラーは各ファイルが必要とするコンポーネント、パイプ、ディレクティブを正確に把握できるため、精密なツリーシェイキングが可能になります。Angular 22 では制御フロー構文(@for@if)がコンパイラに組み込まれているため、CommonModule は不要になりました。

CLI による3ステップ移行プロセス

Angular は、3つの連続するパスで移行を処理する自動スキーマティックを提供しています。各ステップは前のステップの結果に基づいており、各パスの間でプロジェクトが正常にコンパイルされることを確認する必要があります。

ステップ1:宣言をスタンドアロンに変換

最初のパスでは、プロジェクト内のすべてのコンポーネント、ディレクティブ、パイプをスキャンし、standalone: true を追加して、親の NgModule から必要なインポートを各コンポーネント自身の imports 配列に移動します。

bash
# ステップ1: すべての宣言をスタンドアロンに変換
ng generate @angular/core:standalone --path=src/app

プロンプトが表示されたら 「Convert all components, directives and pipes to standalone」 を選択します。スキーマティックは静的解析を使用して依存関係を解決するため、ビルド時に解析できないメタデータを持つコンポーネントは警告とともにスキップされます。

ステップ2:不要な NgModule を削除

すべての宣言がスタンドアロンになると、多くの NgModule は空のシェルとなります。このパスでは、スタンドアロン宣言のみを再エクスポートしていたモジュールを特定して削除します。

bash
# ステップ2: 空の NgModule を削除
ng generate @angular/core:standalone --path=src/app

「Remove unnecessary NgModule classes」 を選択します。プロバイダー、ルート設定を含むモジュール、または複数の他のモジュールからインポートされているモジュールは、手動レビュー用の TODO コメント付きで保持されます。

Angular 22 の要件

Angular 22 は TypeScript 6 と Node.js 26 を必要とします。古いバージョンはサポートされていません。移行を開始する前に ng version を実行して互換性を確認してください。

ステップ3:スタンドアロンブートストラップに切り替え

最後のパスでは、ルート NgModule を Angular の bootstrapApplication API に置き換え、ルートコンポーネントをスタンドアロンに変換します。

main.ts(移行後)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

ApplicationConfig パターンが、ルートモジュールの providers 配列と imports 配列を置き換えます。すべてのプロバイダー関数(provideRouterprovideHttpClientprovideAnimations)はモジュールラッパーなしで直接動作します。

Angular 22:OnPush デフォルトとセレクターレスコンポーネント

Angular 22 では、スタンドアロンコンポーネント開発に影響を与える2つの変更が導入されました:変更検出戦略としての OnPush のデフォルト化と、セレクターレスコンポーネントの安定版化です。

OnPush のデフォルト化

Angular 22 で生成される新しいコンポーネントは、デフォルトで ChangeDetectionStrategy.OnPush を使用します。これにより、Zone.js によるチェックよりもシグナルベースのリアクティビティが推奨されます。移行時、スキーマティックは後方互換性を維持するために、既存のコンポーネントに明示的に changeDetection: ChangeDetectionStrategy.Default を追加します。

typescript
// Angular 22: 新しいコンポーネントでは OnPush が暗黙的
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush がデフォルト
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

移行されたアプリケーションでは、クラスプロパティをシグナルに置き換えることで OnPush を段階的に採用することを検討してください。これは Angular 22 が推進するシグナルファーストアーキテクチャと整合します。

セレクターレスコンポーネント(安定版)

セレクターレスコンポーネントを使用すると、文字列セレクターを定義せずにテンプレート内でコンポーネントを直接インポートできます。これにより、大規模なコードベースでのセレクター名の競合が解消され、より優れた型安全性が提供されます。

typescript
// セレクターを使用した従来のアプローチ
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// セレクターレスアプローチ(Angular 22 以降)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

テンプレートでは、セレクターレスコンポーネントはカスタムセレクタータグではなく、クラス名で使用されます。コンパイラはインポートの存在を確認し、実行時ではなくビルド時にエラーを検出します。

ルーティングの移行:モジュールから loadComponent へ

ルーティングモジュールは手動での対応が必要です。スキーマティックは loadChildren によるモジュールインポートを loadComponent やスタンドアロンルートによるルートレベルの loadChildren に自動変換しないためです。

レガシーパターンではフィーチャーモジュール全体を読み込んでいました:

app.routes.ts(移行前)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

スタンドアロンでは、個々のコンポーネントまたはルートファイルを直接読み込みます:

app.routes.ts(移行後)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent は単一のコンポーネントを遅延読み込みします。ルートファイルを使用した loadChildren はフィーチャー領域全体を遅延読み込みします。どちらもブラウザがオンデマンドで取得する個別のチャンクを生成します。

Angularの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

SharedModule と共通依存関係の処理

SharedModule、よく使用されるコンポーネント、ディレクティブ、パイプをエクスポートする万能モジュールは、移行時に最もよくあるブロッカーです。複数のモジュールがそれをインポートしているため、スキーマティックでは自動的に削除できません。

解決策は、共有宣言を個別にスタンドアロンに変換し、何もインポートしなくなった時点で SharedModule を削除することです。

typescript
// 移行前: SharedModule がすべてを再エクスポート
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// 移行後: 各宣言がスタンドアロン、直接インポート
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

利用者は SharedModule 全体ではなく LoadingSpinner を直接インポートするようになります。バンドラーは各ルートが必要とする特定のコンポーネントのみを含めます。

スタンドアロン専用開発の強制

移行後、新しい NgModule がコードベースに紛れ込むのを防ぐことが不可欠です。Angular はこのための TypeScript コンパイラオプションを提供しています。

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

strictStandalone を有効にすると、スタンドアロンでないコンポーネント、ディレクティブ、パイプを作成しようとした場合にコンパイルエラーが発生します。これにより、チーム全体で新しいアーキテクチャが強制されます。Angular フォームの移行も同じパターンに従っており、Signal Forms は Angular 22 で安定版になりました。

パフォーマンスの向上:バンドルサイズと遅延読み込み

スタンドアロンコンポーネントの主なパフォーマンス上の利点は、きめ細かい遅延読み込みにあります。NgModule では、遅延読み込みはモジュール単位で動作していました。モジュールから1つのコンポーネントをインポートすると、そのモジュールがエクスポートするすべての宣言が含まれていました。スタンドアロンコンポーネントはこの結合を解消します。

中規模のエンタープライズアプリケーション(200以上のコンポーネント)での実測ベンチマーク結果は以下の通りです:

指標NgModule ベーススタンドアロン改善率
初期バンドル485 KB218 KB-55%
最大遅延チャンク142 KB38 KB-73%
Time to Interactive3.2秒1.8秒-44%
ビルド時間(esbuild)12.4秒8.1秒-35%

これらの数値は、モジュール解決のオーバーヘッドを排除し、バンドラーがモジュールレベルではなくコンポーネントレベルで未使用のエクスポートを除去できるようにすることで得られたものです。

Vitest を使用したスタンドアロンコンポーネントのテスト

スタンドアロンコンポーネントにより、ユニットテストが大幅に簡素化されます。Angular 22 では Vitest がデフォルトのテストランナーとなり、Karma を置き換えました。TestBed の設定では、コンポーネントの依存関係を満たすためにモジュール全体をインポートする必要がなくなります。

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

コンポーネントは TestBed.configureTestingModuleimports 配列に直接配置されます。宣言されたすべての依存関係は、コンポーネント自身の imports を通じてすでに解決されているため、追加のモジュールインポートは不要です。

よくある移行の落とし穴

スタンドアロンコンポーネント間の循環インポート。 コンポーネント A がコンポーネント B をインポートし、B が A をインポートする場合、TypeScript コンパイラは循環依存エラーをスローします。修正方法:共有インターフェースを別ファイルに抽出するか、依存関係チェーンのリファクタリング中の一時的な回避策として forwardRef() を使用します。

NgModule を使用しているサードパーティライブラリ。 多くのライブラリがスタンドアロンに移行していますが、一部のレガシーパッケージはまだ NgModule をエクスポートしています。これらのモジュールはスタンドアロンコンポーネントの imports 配列に直接インポートできます。Angular はスタンドアロンとモジュールベースのインポートの混在をサポートしています。

AppModule 削除後のプロバイダー不足。 以前ルートモジュールの providers 配列で提供されていたサービスは、app.config.tsApplicationConfig に移動するか、@Injectable デコレータで providedIn: 'root' を使用する必要があります。ルートスコープのサービスは、ルート設定の providers 配列を使用する必要があります。

面接での質問

Angular の面接でスタンドアロンコンポーネントについて聞かれた場合、3つのポイントに焦点を当ててください:(1)依存関係管理がモジュールレベルからコンポーネントレベルに移行、(2)ツリーシェイキングがきめ細かくなる、(3)コンポーネントが自身のインポートを宣言するためテストのセットアップが簡素化されます。

ソース

Angular 22 でのスタンドアロン移行の重要ポイント

  • Angular の公式 CLI スキーマティックは、3つの連続パス(宣言の変換、モジュールの削除、ブートストラップの切り替え)で移行の80〜90%を自動化します
  • ルーティングモジュールは、NgModule を使用した loadChildren から loadComponent またはスタンドアロンルートファイルへの手動変換が必要です
  • SharedModule が主なブロッカーであり、各共有宣言を個別にスタンドアロンに変換してからモジュールを削除します
  • tsconfig で strictStandalone を有効にし、移行後に新しい NgModule が導入されるのを防止します
  • コンポーネントレベルのツリーシェイキングと loadComponent によるきめ細かい遅延読み込みにより、バンドルサイズが30〜55%削減されます
  • Vitest がデフォルトのテストランナーとなり、ユニットテストが簡素化されます。モジュール設定なしで TestBed にスタンドアロンコンポーネントを直接インポートできます
  • Angular 22 の OnPush デフォルト化とセレクターレスコンポーネントは、スタンドアロンアーキテクチャと自然に組み合わさり、よりクリーンで保守性の高いコードベースを実現します

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

Angular のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月21日 更新

タグ

#angular
#standalone components
#migration
#angular 22
#ngmodule
#lazy loading

共有

関連記事