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.tsのproviders側で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.ts の providers に登録
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. やりがちなミスとアンチパターン
最後に、スタンドアローンを使い始めたときにハマりがちなポイントをまとめておきます。
-
imports の書き忘れ
-
*ngIf/*ngForが効かない
→CommonModuleorNgIf/NgForを入れ忘れているケースがほとんど。
-
-
shared の沼
- 何でもかんでも
shared/に放り込むと、そのうちカオスになります。 - 「複数の feature から本当に参照されるものだけ shared へ」が鉄則。
- 何でもかんでも
-
providers を全部 root に載せる
- なんでも
providedIn: 'root'にするとテストも切りづらくなります。 - ページ専用・機能専用のサービスは、そのページ/機能のコンポーネントに providers で紐づける方がスッキリします。
- なんでも
-
NgModule をなんとなく残し続ける
- 既存資産は仕方ないですが、「新規画面だけでもスタンドアローンにする」だけで構成がだいぶクリアになります。
- 公式の migration ツールもあるので、少しずつ移行していくのが現実的です。
おわりに
スタンドアローンコンポーネントは、
- 学習コストを下げつつ
- 依存関係をコンポーネント単位で閉じて
- ルーティングもシンプルにする
という意味で、今の Angular にとっての「素の形」です。
最初は imports をちまちま書くのが面倒に感じるかもしれませんが、
慣れてくると 「このコンポーネントが何に依存しているかが一目でわかる」 心地よさの方が勝ってきます。
もしあなたのプロジェクトがまだ NgModule 前提であれば、
まずは 1つだけ新規画面をスタンドアローンで書いてみるところから始めてみてください。
その1画面がうまく回り始めたら、きっと「全部これで書きたい」と思うはずです。