0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Angular スタンドアローンコンポーネントについて

Posted at

2025年の「ふつう」を押さえるためのまとめ

最近の Angular をちゃんと追っていると、もう NgModule 前提で新規画面を書くことはほとんどなくなってきました。
代わりに完全に主役になっているのが スタンドアローンコンポーネント です。

この記事では、

  • 「スタンドアローンって言葉は知ってるけど、設計のイメージがまだぼんやり…」
  • 「実際に書くとき、imports やディレクトリ構造をどう考えればいいの?」

という人向けに、手を動かすときに迷いが減るレベルまで、スタンドアローンコンポーネントを掘り下げていきます。

想定読者

  • Angular をある程度触ったことがある(v14 以降を前提)
  • NgModule 時代の構成はなんとなくわかる
  • これからは スタンドアローン前提で画面を書いていきたい

1. スタンドアローンコンポーネントとは?

一言でいうと、

NgModule に属さず、それ単体で完結して動作できるコンポーネント

です。

従来は、

  • コンポーネントを NgModule の declarations に登録して
  • そのモジュールを imports / exports でつないで…

という「モジュール中心」の世界でした。

スタンドアローンでは、発想が逆になります。

  • コンポーネント自身が
    • 使いたいディレクティブ
    • 使いたいパイプ
    • 使いたい他コンポーネント
    • 必要ならモジュール
  • などを 自分の imports プロパティに列挙して完結する

ようになります。

Angular 19 ではコンポーネントが デフォルトで standalone になる予定なので、
「新規はスタンドアローンで書く」が今後のスタンダードになっていきます。

2. スタンドアローンコンポーネントの基本構造

まずは最小例から。

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  standalone: true,
  template: `<h1>Hello Standalone</h1>`,
})
export class HomeComponent {}

ただし、実務で使うコンポーネントはもう少し情報量が増えます。

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

import { UserCardComponent } from '../shared/ui/user-card/user-card.component';
import { DateFormatPipe } from '../shared/pipes/date-format.pipe';

@Component({
  selector: 'app-user-list',
  standalone: true,
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss'],

  // ★ スタンドアローンの要:ここで依存を完結させる
  imports: [
    CommonModule,
    RouterLink,
    UserCardComponent,
    DateFormatPipe,
  ],

  // このコンポーネント配下だけで使いたいサービス
  providers: [],

  // パフォーマンス向上のための基本設定
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {}

各設定のポイント

  • selector
    他テンプレートから <app-user-list> のように使うタグ名。
    ルーティング直下のページは、あえて selector を使わず component で直接指定するケースもあります。

  • standalone
    「このコンポーネントは NgModule には属さない」という宣言。
    v19 以降はデフォルトで true なので、新しめのプロジェクトでは省略されていても驚かなくてOKです。

  • template / templateUrl
    デモや小さい UI はインラインテンプレートで十分ですが、
    実運用の画面は素直に templateUrl に分ける方が読みやすくなります。

  • imports
    スタンドアローン最大のキモ。
    テンプレート内で使うもの(*ngIf / *ngFor / RouterLink / 他コンポーネント / パイプ / ディレクティブ…)を
    ここに全部書きます。

  • providers
    このコンポーネント(+配下の子コンポーネント)専用のサービスを差し込みたいときに使用。
    「このページ専用の Facade」などに向いています。

  • changeDetection
    実務では OnPush を標準にしておくのがおすすめです。
    設計が乱暴でなければ、パフォーマンスがかなり安定します。

3. imports をどう設計するか?

imports は、コンポーネントが 「自分は何を使うのか」 を宣言する場所です。

3.1 Angular のビルトイン

import { NgIf, NgFor } from '@angular/common';
import { RouterLink, RouterOutlet } from '@angular/router';

@Component({
  standalone: true,
  // ...
  imports: [
    NgIf,
    NgFor,
    RouterLink,
    RouterOutlet,
  ],
})
export class AppComponent {}
  • ざっくりでいいなら CommonModule 丸ごと import でもOK
  • もう少し丁寧にやるなら NgIf, NgFor を個別 import
  • RouterLink / RouterOutlet もコンポーネントに直接 import できます

3.2 再利用コンポーネント/パイプ/ディレクティブ

imports: [
  CommonModule,
  RouterLink,
  UserCardComponent,
  UserAvatarComponent,
  DateFormatPipe,
  EmailValidatorDirective,
]
  • UI コンポーネントはスタンドアローンで切り出しておくと、
    どの feature からも import するだけで使い回せます。

3.3 サードパーティとの付き合い

ライブラリ側がまだ NgModule ベースの場合:

  • 単純に imports に突っ込めるものもある
  • あるいは app.config.tsproviders 側で importProvidersFrom する必要があるものもある

例えば:

import { importProvidersFrom } from '@angular/core';
import { SomeLegacyModule } from 'some-legacy-lib';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    importProvidersFrom(SomeLegacyModule),
  ],
};

4. ディレクトリ構造:スタンドアローン時代の「定番」

スタンドアローンコンポーネントは、1ファイルで完結している分、フォルダ構成も重要になります。

4.1 機能(feature)単位で切る構成例

src/
  main.ts
  app/
    app.component.ts
    app.config.ts
    app.routes.ts

    core/                     # アプリ全体で1つだけのもの
      layout/
        layout.component.ts
      services/
        auth.service.ts
        api-client.service.ts

    shared/                   # 再利用コンポーネント・パイプ等
      ui/
        button/
          button.component.ts
          button.component.html
        card/
          card.component.ts
      directives/
        autofocus.directive.ts
      pipes/
        date-format.pipe.ts

    features/
      home/
        home.page.ts
        home.page.html
        home.page.scss
        index.ts

      user/
        pages/
          user-list.page.ts
          user-list.page.html
          user-detail.page.ts
          user-detail.page.html
        components/
          user-card.component.ts
        services/
          user-data.service.ts
        models/
          user.model.ts
        user.routes.ts
        index.ts

ざっくりいうと、

  • core:アプリ全体に1つしかないもの(レイアウト、グローバルサービスなど)
  • shared:どの機能からも使える「汎用パーツ」
  • features/:ドメインや画面ごとのまとまり

という三層構造です。

5. app.config.ts と bootstrapApplication

NgModule 時代の AppModule の役割は、
スタンドアローン時代では bootstrapApplication + app.config.ts に分割されます。

5.1 main.ts

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));

5.2 app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withFetch()),
    // グローバルに共有したいサービス類をここに登録
  ],
};
  • ルーティング設定(provideRouter
  • HTTPクライアント設定(provideHttpClient
  • 全画面で共有するサービス

などをここに集約していきます。

6. DI(依存性注入)のスコープ設計

スタンドアローンになっても DI の概念は同じですが、
「どこに providers を書くか」 が変わります。

6.1 アプリ全体で共有したいサービス

app.config.tsproviders に登録

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    AuthService,         // 全画面共通
    FeatureFlagService,  // グローバルな設定
  ],
};

6.2 特定の feature だけで共有したいサービス

→ feature の root となる ページコンポーネントproviders に書く

@Component({
  standalone: true,
  templateUrl: './user-page.component.html',
  imports: [CommonModule, RouterOutlet],
  providers: [UserFacade],
})
export class UserPageComponent {}
  • UserPageComponent 配下のコンポーネントからは同じ UserFacade を共有できます。

6.3 1コンポーネント専用にしたい場合

→ そのコンポーネントの providers にだけ書き、
@Injectable({ providedIn: 'root' }) は付けない。

7. ルーティングとスタンドアローンの組み合わせ

スタンドアローンでは コンポーネントを直接ルーティングに載せられる のも大きな特徴です。

7.1 app.routes.ts

import { Routes } from '@angular/router';
import { HomePage } from './features/home/home.page';

export const routes: Routes = [
  {
    path: '',
    component: HomePage,
  },
  {
    path: 'users',
    loadChildren: () =>
      import('./features/user/user.routes').then((m) => m.USER_ROUTES),
  },
];

7.2 feature 側のルート(user.routes.ts)

import { Routes } from '@angular/router';
import { UserListPage } from './pages/user-list.page';
import { UserDetailPage } from './pages/user-detail.page';

export const USER_ROUTES: Routes = [
  {
    path: '',
    component: UserListPage,
  },
  {
    path: ':id',
    component: UserDetailPage,
  },
];
  • どちらも NgModule なしで完結しているのがポイントです。

8. 「ページ」と「UIコンポーネント」を分ける実践パターン

スタンドアローンでは、
「ページ(ルーティング直下)」と「UIコンポーネント」 を分けるとすごく設計しやすくなります。

8.1 ディレクトリ例

user/
  pages/
    user-edit.page.ts
    user-edit.page.html
  components/
    user-form.component.ts
    user-form.component.html

8.2 ページ側(user-edit.page.ts)

@Component({
  standalone: true,
  templateUrl: './user-edit.page.html',
  imports: [CommonModule, UserFormComponent],
  providers: [UserEditFacade],
})
export class UserEditPage {
  vm$ = this.facade.vm$;

  constructor(private facade: UserEditFacade) {}

  onSubmit(formValue: UserFormValue) {
    this.facade.save(formValue);
  }
}

8.3 UIフォーム側(user-form.component.ts)

@Component({
  standalone: true,
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
  imports: [ReactiveFormsModule, CommonModule],
})
export class UserFormComponent {
  form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
  });

  @Output() submitForm = new EventEmitter<UserFormValue>();

  onSubmit() {
    if (this.form.valid) {
      this.submitForm.emit(this.form.value as UserFormValue);
    }
  }
}
  • ページ:ルーティング・DI・状態管理に専念
  • UIコンポーネント:フォーム構造と見た目に専念

この分け方は、スタンドアローンの思想とも相性が良く、
テストやコンポーネントの再利用もしやすくなります。

9. テストでの扱い(TestBed の書き方が変わる)

スタンドアローンコンポーネントのテストは、
declarations ではなく imports に載せるのがポイントです。

import { TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';

describe('UserListComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserListComponent],  // ← ここ
    }).compileComponents();
  });

  it('should create', () => {
    const fixture = TestBed.createComponent(UserListComponent);
    const component = fixture.componentInstance;
    expect(component).toBeTruthy();
  });
});

NgModule 時代のクセで declarations: [UserListComponent] と書いてしまうと、
「スタンドアローンなのに宣言しようとして怒られる」ので注意です。

10. やりがちなミスとアンチパターン

最後に、スタンドアローンを使い始めたときにハマりがちなポイントをまとめておきます。

  1. imports の書き忘れ

    • *ngIf / *ngFor が効かない
      CommonModule or NgIf / NgFor を入れ忘れているケースがほとんど。
  2. shared の沼

    • 何でもかんでも shared/ に放り込むと、そのうちカオスになります。
    • 「複数の feature から本当に参照されるものだけ shared へ」が鉄則。
  3. providers を全部 root に載せる

    • なんでも providedIn: 'root' にするとテストも切りづらくなります。
    • ページ専用・機能専用のサービスは、そのページ/機能のコンポーネントに providers で紐づける方がスッキリします。
  4. NgModule をなんとなく残し続ける

    • 既存資産は仕方ないですが、「新規画面だけでもスタンドアローンにする」だけで構成がだいぶクリアになります。
    • 公式の migration ツールもあるので、少しずつ移行していくのが現実的です。

おわりに

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

  • 学習コストを下げつつ
  • 依存関係をコンポーネント単位で閉じて
  • ルーティングもシンプルにする

という意味で、今の Angular にとっての「素の形」です。

最初は imports をちまちま書くのが面倒に感じるかもしれませんが、
慣れてくると 「このコンポーネントが何に依存しているかが一目でわかる」 心地よさの方が勝ってきます。

もしあなたのプロジェクトがまだ NgModule 前提であれば、
まずは 1つだけ新規画面をスタンドアローンで書いてみるところから始めてみてください。
その1画面がうまく回り始めたら、きっと「全部これで書きたい」と思うはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?