Angular Router(@angular/router
)は、Angular アプリのナビゲーションを管理する公式ライブラリで、フレームワークの中核機能のひとつ。Angular CLI で作成したプロジェクトには標準で含まれている。
SPA でルーティングが必要な理由は、通常の Web サイトのようにページごとにサーバーへリクエストして HTML 全体を差し替えるのではなく、最初の index.html
を取得した後はクライアント側のルーターが URL に応じて表示内容を切り替えるため。これによりページ全体をリロードせずに、URL に基づいたコンテンツを動的に差し替えられる。
Angular のルーティングは主に次の3要素で構成される:
- Routes: 特定の URL にアクセスしたときにどのコンポーネントを表示するかを定義する
- Outlets: アクティブなルートに応じてコンポーネントを動的に読み込み・描画するテンプレート上のプレースホルダー
- Links: ページ全体をリロードせずにルート間を移動するための仕組み
さらに Angular Router は以下の追加機能も提供する:
- ネストされたルート
- プログラムによるナビゲーション
- ルートパラメータ・クエリ・ワイルドカード
-
ActivatedRoute
によるルート情報取得 - ビューの遷移効果
- ナビゲーションガード
ルートを定義する
ルートとは
Angular におけるルートは、特定の URL パスやパターンに対して「どのコンポーネントを描画するか」や「ユーザーがその URL に移動したときにどんな処理を行うか」を定義するオブジェクト。
import { AdminPage } from './app-admin/app-admin.component';
const adminPage = {
path: 'admin',
component: AdminPage
}
このルートの場合、ユーザーが /admin
パスにアクセスすると、アプリケーションは AdminPage
コンポーネントを表示する。
アプリケーションでルートを管理する
ほとんどのプロジェクトでは、ルート定義は routes
を含む別ファイルにまとめる。
ルートの集合は以下のように表す:
import { Routes } from '@angular/router';
import { HomePage } from './home-page/home-page.component';
import { AdminPage } from './about-page/admin-page.component';
export const routes: Routes = [
{
path: '',
component: HomePage,
},
{
path: 'admin',
component: AdminPage,
},
];
Routes
型の配列で、それぞれの要素が URL パスと表示するコンポーネントを対応付ける。
Angular CLI でプロジェクトを生成した場合、ルートは src/app/app.routes.ts
で定義される。
アプリケーションにルーターを追加する
Angular CLI を使わずにアプリをブートストラップする場合、bootstrapApplication
に構成オブジェクトを渡して providers
を設定できる。
その中で provideRouter
にルート定義を渡せば、アプリにルーターを組み込める。
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes)
]
});
これでルーティングが有効になり、定義した URL パスごとにコンポーネントを切り替えられる。
ルート URL パス
静的な URL パス
静的な URL パスは、動的パラメータを含まず、あらかじめ決められた固定のパスにだけ一致するルート。
パス文字列が完全一致したときに、常に同じコンポーネントや処理を返す。
例:
/admin
/blog
/settings/account
ルートパラメーターで URL パスを定義する
パラメーター化された URL を使うと、同じコンポーネントに複数の URL を割り当てつつ、URL 内の値に応じて動的にデータを表示できる。
ルート定義では path
にコロン :
を付けてパラメーターを記述する。
import { Routes } from '@angular/router';
import { UserProfile } from './user-profile/user-profile';
const routes: Routes = [
{ path: 'user/:id', component: UserProfile }
];
この場合 /user/leeroy
や /user/jenkins
のような URL はすべて UserProfile
コンポーネントをレンダリングし、:id
の値を読み取ってデータ取得などに利用できる。
ルートパラメーター名は以下の規則に従う必要がある
- 先頭はアルファベット (a–z, A–Z)
- 使用できる文字は 英字・数字・アンダースコア
_
・ハイフン-
複数のパラメーターを持つパスも定義できる。
import { Routes } from '@angular/router';
import { UserProfile } from './user-profile/user-profile.component';
import { SocialMediaFeed } from './user-profile/social–media-feed.component';
const routes: Routes = [
{ path: 'user/:id/:social-media', component: SocialMediaFeed },
{ path: 'user/:id/', component: UserProfile },
];
ワイルドカード
ワイルドカードルートは、特定のルート定義に一致しないすべてのパスをキャッチするために使う。path: '**'
と書くことで実現でき、典型的には「ページが見つかりません」用のコンポーネントを表示する。
import { Routes } from '@angular/router';
import { Home } from './home/home.component';
import { UserProfile } from './user-profile/user-profile.component';
import { NotFound } from './not-found/not-found.component';
const routes: Routes = [
{ path: 'home', component: Home },
{ path: 'user/:id', component: UserProfile },
{ path: '**', component: NotFound } // 最後に定義する必要がある
];
この場合、/home
や /user/123
以外の URL にアクセスすると、NotFound
コンポーネントが表示される。
通常、ワイルドカードはルート配列の最後に配置する。
AngularがURLを照合する方法
Angular のルートは「最初に一致したものを優先する」戦略を採用するため、ルートの順序が重要。
より具体的なルートを、より一般的・動的なルートやワイルドカードの前に配置する必要がある。
const routes: Routes = [
{ path: '', component: HomeComponent }, // 空パス
{ path: 'users/new', component: NewUserComponent }, // 静的で最も具体的
{ path: 'users/:id', component: UserDetailComponent },// 動的
{ path: 'users', component: UsersComponent }, // 静的だが具体的でない
{ path: '**', component: NotFoundComponent } // ワイルドカード、必ず最後
];
動作例: /users/new
にアクセスした場合
-
''
をチェック → 不一致 -
'users/new'
をチェック → 一致!ここで処理停止 -
'users/:id'
は評価されない -
'users'
も評価されない -
'**'
も評価されない
具体的なルートを先に、一般的なルートやワイルドカードを後に置くことが正しいルート定義の基本。
ルートコンポーネントの読み込み戦略
Angular では、コンポーネントがどのタイミングで読み込まれるかを理解することが、応答性の高いアプリ構築に不可欠。
読み込みには主に2つの戦略がある:
- 即時読み込み (Eagerly loaded): アプリ起動時にすぐ読み込まれるコンポーネント
- 遅延読み込み (Lazily loaded): 必要になったときにのみ読み込まれるコンポーネント
それぞれ、用途やパフォーマンス最適化の観点で異なる利点を持つ。
即時読み込みされるコンポーネント
component
プロパティでルートを定義すると、参照されたコンポーネントはそのルート定義と同じ JavaScript バンドルに即時読み込みされる。
import { Routes } from "@angular/router";
import { HomePage } from "./components/home/home-page";
import { LoginPage } from "./components/auth/login-page";
export const routes: Routes = [
{ path: "", component: HomePage },
{ path: "login", component: LoginPage }
];
この場合、HomePage
と LoginPage
のコードは初期バンドルに含まれ、アプリ起動時にブラウザがすべてダウンロードして解析する必要がある。
メリット: コンポーネントはすぐに利用可能で、ページ間の遷移がシームレスになる
デメリット: 初期ページ読み込み時の JavaScript サイズが大きくなると、ロード時間が遅くなる
遅延読み込みされるコンポーネント
loadComponent
プロパティを使うと、ルートがアクティブになった時点でのみ対応するコンポーネントの JavaScript を遅延読み込みできる。
import { Routes } from "@angular/router";
export const routes: Routes = [
{
path: 'login',
loadComponent: () => import('./components/auth/login-page').then(m => m.LoginPage)
},
{
path: '',
loadComponent: () => import('./components/home/home-page').then(m => m.HomePage)
}
];
-
loadComponent
には Promise を返すローダー関数を指定する - 通常は JavaScript の動的
import()
を使う - 遅延読み込みにより、初期バンドルから大部分の JavaScript を削除でき、初期ロードが高速化される
- 必要になったときにだけルーターが個別のチャンクを取得してコンポーネントを表示する
即時ルートと遅延ルートの使い分け
即時ルートと遅延ルートの使い分けは、ページの重要度やパフォーマンス要件で判断する。
- 即時読み込み: プライマリランディングページや初期表示に必須のコンポーネントに推奨
- 遅延読み込み: 初期表示に必須でないページやユーザーが後からアクセスするページに推奨
遅延ルートは初期ロードを軽くできるが、ユーザーがアクセスしたタイミングで追加のデータ要求が発生する。また、複数レベルでのネストされた遅延読み込みは、パフォーマンスに大きな影響を与える可能性がある。
リダイレクト
コンポーネントを直接表示する代わりに、特定のルートにアクセスしたユーザーを別のルートへ自動で転送するようルートを定義できる。
import { BlogComponent } from './home/blog.component';
const routes: Routes = [
{
path: 'articles',
redirectTo: '/blog',
},
{
path: 'blog',
component: BlogComponent
},
];
たとえば /articles
へのアクセスを /blog
にリダイレクトするように設定すると、古いリンクやブックマークを使ってアクセスしたユーザーも適切なページに誘導できる。これにより「ページが見つかりません」に飛ばすのではなく、ユーザー体験を損なわずに済む。
ページタイトル
各ルートにはタイトルを設定でき、ルートがアクティブになると Angular が自動的にページタイトルを更新する。アクセシブルな体験を作るために、アプリケーションでは常に適切なページタイトルを定義することが推奨される。
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
const routes: Routes = [
{ path: '', component: HomeComponent, title: 'Home Page' },
{ path: 'about', component: AboutComponent, title: 'About Us' },
];
動的にタイトルを設定する場合は ResolveFn
を使ってリゾルバー関数を指定できる。
const titleResolver: ResolveFn<string> = (route) => route.queryParams['id'];
const routes: Routes = [
{
path: 'products',
component: ProductsComponent,
title: titleResolver,
}
];
ルートタイトルは TitleStrategy
抽象クラスを継承するカスタムサービスを使っても設定可能で、デフォルトでは Angular が DefaultTitleStrategy
を使用する。
依存性注入のためのルートレベルプロバイダー
各ルートには providers
プロパティを使って、そのルートに依存するサービスや値を依存性注入で提供できる。これは例えば、ユーザーが管理者かどうかに応じて異なるサービスを利用するアプリケーションで便利。
import { Route } from '@angular/router';
import { AdminService, ADMIN_API_KEY } from './admin/admin.service';
import { AdminUsersComponent } from './admin/admin-users.component';
import { AdminTeamsComponent } from './admin/admin-teams.component';
export const ROUTES: Route[] = [
{
path: 'admin',
providers: [
AdminService,
{ provide: ADMIN_API_KEY, useValue: '12345' }
],
children: [
{ path: 'users', component: AdminUsersComponent },
{ path: 'teams', component: AdminTeamsComponent },
],
},
];
この例では、admin
パスに関連付けられた ADMIN_API_KEY
はその子ルートだけで利用可能で、他のルートからはアクセスできない。
ルートにデータを関連付ける
ルートデータを使うと、ルートに追加情報を持たせてコンポーネントの動作を制御できる。
ルートデータには、あらかじめ定義された静的データと、実行時の条件に応じて変化する動的データの2種類がある。
静的データ
data
プロパティを使うと、ルートに任意の静的データを関連付けられ、分析トラッキングや権限などのルート固有メタデータを一元管理できる。
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
const routes: Routes = [
{ path: '', component: HomeComponent, data: { analyticsId: '123' } },
{ path: 'about', component: AboutComponent, data: { analyticsId: '456' } }
];
この例では、ホームページとアバウトページにそれぞれ analyticsId
が設定され、コンポーネント内でページトラッキング分析に利用できる。静的データは ActivatedRoute
を注入して読み取れる。
動的データ
動的データについては後の章で詳しく記載する。
ネストされたルート
ネストされたルート(子ルート)は、URL に応じてサブビューを切り替える複雑なナビゲーションを管理するために使う。children
プロパティを使うと、任意のルートに子ルートを追加できる。
const routes: Routes = [
{
path: 'product/:id',
component: ProductComponent,
children: [
{ path: 'info', component: ProductInfoComponent },
{ path: 'reviews', component: ProductReviewsComponent }
]
}
];
この例では、製品ページでユーザーが URL に応じて製品情報かレビューを表示できる。子ルートをレンダリングするには親コンポーネントに <router-outlet>
を配置する
<article>
<h1>Product {{ id }}</h1>
<router-outlet></router-outlet>
</article>
こうすることで、子ルートに一致する URL にナビゲートしても、親のビュー全体を再描画せず、ネストされた <router-outlet>
のみが更新される。
アウトレットにルートを表示する
RouterOutlet
ディレクティブは、ルーターが現在の URL に対応するコンポーネントを挿入する場所を示すプレースホルダーとして機能する。
例えば、アプリのテンプレートが次のようになっている場合。
<app-header></app-header>
<router-outlet></router-outlet> <!-- ここにルートコンポーネントが挿入される -->
<app-footer></app-footer>
コンポーネント側は以下のように定義できる。
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {}
ルートが以下のように定義されている場合
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductsComponent } from './products/products.component';
const routes: Routes = [
{ path: '', component: HomeComponent, title: 'Home Page' },
{ path: 'products', component: ProductsComponent, title: 'Our Products' }
];
ユーザーが /products
にアクセスすると、Angular は <router-outlet>
の位置に <app-products>
を挿入してレンダリングし、結果は次のようになる。
<app-header></app-header>
<app-products></app-products>
<app-footer></app-footer>
ホームページに戻ると <router-outlet>
内のコンテンツが <app-home>
に置き換わり、レンダリングは以下のようになる。
<app-header></app-header>
<app-home></app-home>
<app-footer></app-footer>
<router-outlet>
は常に DOM に存在し続け、将来のナビゲーションの参照点として使われる。Angular はルーティングされたコンテンツをアウトレット要素の直後に兄弟要素として挿入するため、ナビゲーションごとに全体の DOM を書き換える必要はなく、ヘッダーやフッターはそのまま保持される。
名前付きアウトレットによるセカンダリールート
ページに複数の <router-outlet>
を配置することができ、各アウトレットに名前を割り当てることで、どのルートコンテンツをどのアウトレットに表示するかを指定できる。
<app-header></app-header>
<router-outlet></router-outlet> <!-- デフォルトの primary アウトレット -->
<router-outlet name="read-more"></router-outlet>
<router-outlet name="additional-actions"></router-outlet>
<app-footer></app-footer>
各アウトレットには一意の名前が必要で、動的に変更することはできない。名前を指定しない場合、デフォルトで 'primary'
として扱われる。
ルート定義では outlet
プロパティを使って対象のアウトレットを指定する:
{
path: 'user/:id',
component: UserDetails,
outlet: 'additional-actions'
}
この場合、UserDetails
コンポーネントは 'additional-actions'
という名前のアウトレットにレンダリングされる。
アウトレットのライフサイクルイベント
<router-outlet>
は 4 つのライフサイクルイベントを発行できる。
-
activate
は新しいコンポーネントがインスタンス化されたときに発火 -
deactivate
はコンポーネントが破棄されるときに発火 -
attach
はRouteReuseStrategy
がサブツリーをアウトレットに再アタッチするときに発火 -
detach
はRouteReuseStrategy
がサブツリーをアウトレットからデタッチするときに発火
これらのイベントは標準のイベントバインディング構文でリスナーを追加できる。
<router-outlet
(activate)="onActivate($event)"
(deactivate)="onDeactivate($event)"
(attach)="onAttach($event)"
(detach)="onDetach($event)"
></router-outlet>
$event
には該当するコンポーネントのインスタンスが渡され、ライフサイクルに応じた処理を実装できる。
ルートへのナビゲーション
RouterLink
ディレクティブは、Angular における宣言的ナビゲーション手法で、通常の <a>
タグを使いながらルーターとシームレスに統合できる。これを使うことで、リンククリック時にページ全体をリロードせずにルートを切り替えられる。
RouterLinkの使い方
通常の <a>
タグで href
を使う代わりに、Angular のルーティングを利用する場合は RouterLink
ディレクティブを追加し、目的のパスを指定する。
例:
import { RouterLink } from '@angular/router';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<nav>
<a routerLink="/user-profile">User profile</a>
<a routerLink="/settings">Settings</a>
</nav>
`,
imports: [RouterLink]
})
export class App {}
この方法により、リンククリック時にページ全体をリロードせずに、Angular ルーターが指定したコンポーネントをレンダリングする。
絶対リンクと相対リンクの使用
Angular ルーティングでは、相対 URL を使うことで、現在のルートに対するナビゲーションパスを指定できる。これは、プロトコルやルートドメインを含む絶対 URL とは対照的である。
<!-- 絶対 URL -->
<a href="https://www.angular.dev/essentials">Angular Essentials Guide</a>
<!-- 相対 URL -->
<a href="/essentials">Angular Essentials Guide</a>
上の例では、絶対 URL は https://angular.dev/essentials
の完全なパスを指定しているのに対し、相対 URL はユーザーがすでに正しいドメイン上にいることを前提に /essentials
へのパスのみを示している。相対 URL はルーティング階層の絶対位置を意識する必要がなく、アプリケーション全体での保守が容易なため推奨される。
相対 URL の仕組み
Angular ルーティングでは、相対 URL を指定する際に文字列構文と配列構文の2つを使える。
<!-- 文字列構文 -->
<a routerLink="dashboard">Dashboard</a>
<!-- 配列構文 -->
<a [routerLink]="['dashboard']">Dashboard</a>
文字列を渡すのが、相対URLを定義する最も一般的。
動的パラメーターを扱う場合は配列構文を使用するのが一般的。
<a [routerLink]="['user', currentUserId]">Current User</a>
相対パスはスラッシュ(/
)の有無で解釈が変わる。スラッシュなしは現在の URL に対する相対パス、スラッシュありはルートドメインに対する相対パスとなる。
例として、ユーザーが example.com/settings
にいる場合
<!-- /settings/notifications に移動 -->
<a routerLink="notifications">Notifications</a>
<a routerLink="/settings/notifications">Notifications</a>
<!-- /team/:teamId/user/:userId に移動 -->
<a routerLink="/team/123/user/456">User 456</a>
<a [routerLink]="['/team', teamId, 'user', userId]">Current User</a>
これにより、静的および動的パラメーターを含むルートに柔軟にナビゲーションできる。
ルートへのプログラムによるナビゲーション
RouterLink
がテンプレート上で宣言的ナビゲーションを提供するのに対し、Angular ではアプリケーションの状態やユーザーの操作に応じて動的にナビゲートするためのプログラム的ナビゲーションも提供されている。
Router
サービスを注入すると、TypeScript コード内でルートへの遷移を制御でき、パラメーターの渡し方やナビゲーションのタイミングも柔軟に設定できる。
router.navigate()
router.navigate()
メソッドを使うと、URL パス配列を指定してプログラム的にルート間を遷移できる。単純なナビゲーションから、ルートパラメーターやクエリパラメーターを含む複雑なケースまで対応可能。
import { Router } from '@angular/router';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `<button (click)="navigateToProfile()">View Profile</button>`
})
export class AppDashboard {
private router = inject(Router);
navigateToProfile() {
// シンプルなナビゲーション
this.router.navigate(['/profile']);
// ルートパラメーターを含むナビゲーション
this.router.navigate(['/users', userId]);
// クエリパラメーターを含むナビゲーション
this.router.navigate(['/search'], {
queryParams: { category: 'books', sort: 'price' }
});
}
}
relativeTo
オプションを使うと、ルーティングツリー内で現在のコンポーネントを基準にした相対的なナビゲーションも可能。
import { Router, ActivatedRoute } from '@angular/router';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-user-detail',
template: `
<button (click)="navigateToEdit()">Edit User</button>
<button (click)="navigateToParent()">Back to List</button>
`
})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
// 同じ階層のルートへ
navigateToEdit() {
// /users/123 → /users/123/edit
this.router.navigate(['edit'], { relativeTo: this.route });
}
// 親ルートへ
navigateToParent() {
// /users/123 → /users
this.router.navigate(['..'], { relativeTo: this.route });
}
}
この方法により、現在のルートに対する相対的な遷移や、パラメーター付きナビゲーションを柔軟に制御できる。
router.navigateByUrl()
router.navigateByUrl()
は URL パス文字列を使って直接ナビゲートするメソッドで、配列セグメントではなく完全なパスを指定できる。絶対ナビゲーションや外部から提供されるディープリンクなどのケースに適している。
// 通常のルートナビゲーション
router.navigateByUrl('/products');
// ネストされたルートへ
router.navigateByUrl('/products/featured');
// パラメーターとフラグメントを含む完全な URL
router.navigateByUrl('/products/123?view=details#reviews');
// クエリパラメーター付き
router.navigateByUrl('/search?category=books&sortBy=price');
履歴内の現在の URL を置き換えたい場合は、replaceUrl
オプションを使用できる。
// 履歴の現在の URL を置き換える
router.navigateByUrl('/checkout', {
replaceUrl: true
});
navigateByUrl()
は文字列ベースでの絶対ナビゲーションに便利で、動的に生成された URL でも簡単に遷移できる。
ルートの状態を読み取る
Angular ルーターを使うと、ルートに関連付けられた情報をコンポーネント内で取得・利用できる。これにより、ルートの状態やパラメーターに応じて動的に振る舞う、応答性の高いコンテキスト依存コンポーネントを構築できる。
ActivatedRoute
で現在のルートに関する情報を取得する
ActivatedRoute
は現在のルートに関する情報を提供する Angular のサービスで、コンポーネント内でルートの状態を取得・利用できる。
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product',
})
export class ProductComponent {
private activatedRoute = inject(ActivatedRoute);
constructor() {
console.log(this.activatedRoute);
}
}
代表的なプロパティには以下がある。
-
url
:ルートパスの各部分を文字列配列として表現する Observable -
data
:ルートに設定されたdata
オブジェクトやresolve
ガードで解決された値を含む Observable -
params
:ルート固有の必須・オプションパラメーターを含む Observable -
queryParams
:全ルートで利用可能なクエリパラメーターを含む Observable
これらを活用することで、ルートに応じた動的なコンポーネントの振る舞いを実装できる。
詳しくは、ActivatedRoute
API ドキュメントを参照。
ルートスナップショットを理解する
ページナビゲーションは時間に沿って発生するため、特定の時点でのルーター状態を確認するにはルートスナップショットを使用する。スナップショットは静的で、後の変更は反映されない。
ルートスナップショットには、ルートパラメーター、クエリパラメーター、データ、子ルートなどの情報が含まれる。
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ ... })
export class UserProfileComponent {
readonly userId: string;
private route = inject(ActivatedRoute);
constructor() {
// URL例: https://www.angular.dev/users/123?role=admin&status=active#contact
// スナップショットからルートパラメーターを取得
this.userId = this.route.snapshot.paramMap.get('id');
// その他のスナップショット情報
const snapshot = this.route.snapshot;
console.log({
url: snapshot.url,
params: snapshot.params, // {id: '123'}
queryParams: snapshot.queryParams, // {role: 'admin', status: 'active'}
});
}
}
これにより、ナビゲーション時の固定状態に基づいてコンポーネントを初期化したり、必要なデータを取得したりできる。
ルート上のパラメーターを読み取る
Angular では、開発者はルートに関連する情報として、ルートパラメーターとクエリパラメーターの二種類を利用できる。ルートパラメーターは URL の特定部分に埋め込まれる必須またはオプションの値で、クエリパラメーターは URL の末尾に ?key=value
の形式で付加される追加情報として扱われる。
ルートパラメーター
ルートパラメーターを使うと、URL を通じてコンポーネントにデータを渡せる。ユーザー ID や製品 ID など、URL 内の識別子に基づいて特定のコンテンツを表示したい場合に有効。パラメーターはルート定義で名前の前にコロン :
を付けて指定する。
import { Routes } from '@angular/router';
import { ProductComponent } from './product/product.component';
const routes: Routes = [
{ path: 'product/:id', component: ProductComponent }
];
コンポーネント側では route.params
を購読して値を取得できる。
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-detail',
template: `<h1>Product Details: {{ productId() }}</h1>`,
})
export class ProductDetailComponent {
productId = signal('');
private activatedRoute = inject(ActivatedRoute);
constructor() {
this.activatedRoute.params.subscribe(params => {
this.productId.set(params['id']);
});
}
}
これにより、URL に基づいて動的にコンテンツを表示可能になる。
クエリパラメーター
クエリパラメーターは、ルート構造に影響を与えずに URL を介してオプションデータを渡す方法を提供する。ルートパラメーターとは異なり、ナビゲーション間で永続化でき、フィルタリングやソート、ページネーションなどステートフルな UI に適している。
// 単一パラメーター
router.navigate(['/products'], { queryParams: { category: 'electronics' } });
// 複数パラメーター
router.navigate(['/products'], {
queryParams: { category: 'electronics', sort: 'price', page: 1 }
});
コンポーネント側では route.queryParams
を購読してアクセスできる。
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-product-list',
template: `
<div>
<select (change)="updateSort($event)">
<option value="price">Price</option>
<option value="name">Name</option>
</select>
<!-- Products list -->
</div>
`
})
export class ProductListComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
constructor() {
this.route.queryParams.subscribe(params => {
const sort = params['sort'] || 'price';
const page = Number(params['page']) || 1;
this.loadProducts(sort, page);
});
}
updateSort(event: Event) {
const sort = (event.target as HTMLSelectElement).value;
this.router.navigate([], {
queryParams: { sort },
queryParamsHandling: 'merge' // 他のクエリパラメーターを保持
});
}
loadProducts(sort: string, page: number) {
// 製品リストを取得して表示
}
}
この構造により、ユーザーがソートオプションを変更すると URL のクエリパラメーターが更新され、それに応じて製品リストがリアクティブに再描画される。
ルートパラメーターは必須かつリソース識別向け、クエリパラメーターはオプションの条件付けや UI 状態管理向けという使い分けになる。
RouterLinkActive
でアクティブな現在のルートを検出する
RouterLinkActive
は、現在アクティブなルートに基づいてナビゲーション要素のスタイルを動的に更新するためのディレクティブ。ユーザーがどのページにいるかを視覚的に示すのに便利で、通常ナビゲーションバーのリンクに使用される。
例えば <a routerLink="/about" routerLinkActive="active-button">About</a>
とすると、URL が /about
のときに active-button
クラスが自動的に追加される。ariaCurrentWhenActive="page"
を併用すると、スクリーンリーダーなどの支援技術に対して、現在のページであることを伝えることができる。
複数クラスを適用したい場合は、スペース区切りの文字列か配列で指定できる。
<!-- スペース区切り -->
<a routerLink="/user/bob" routerLinkActive="class1 class2">Bob</a>
<!-- 配列構文 -->
<a routerLink="/user/bob" [routerLinkActive]="['class1', 'class2']">Bob</a>
RouterLinkActive
によって自動で ariaCurrentWhenActive
が設定されるが、異なる値を使用したい場合は ariaCurrentWhenActive
を明示的に指定する必要がある。
ルートマッチング戦略
RouterLinkActive
はデフォルトで部分一致を使用するため、現在の URL がリンクのルートを含む場合、祖先リンクにもクラスが適用される。
<a [routerLink]="['/user/jane']" routerLinkActive="active-link">
User
</a>
<a [routerLink]="['/user/jane/role/admin']" routerLinkActive="active-link">
Role
</a>
この例では、ユーザーが /user/jane/role/admin
にアクセスすると、User
と Role
の両方のリンクに active-link
クラスが付く。
RouterLinkActive
を厳密なルート一致にのみ適用する
routerLinkActiveOptions
に { exact: true }
を設定すると、現在の URL がリンクのルートと完全に一致する場合にのみクラスが適用される。部分一致の場合は祖先リンクにクラスが付くが、exact: true
を指定するとそれを防げる。
<a [routerLink]="['/user/jane']"
routerLinkActive="active-link"
[routerLinkActiveOptions]="{exact: true}"
>
User
</a>
<a [routerLink]="['/user/jane/role/admin']"
routerLinkActive="active-link"
[routerLinkActiveOptions]="{exact: true}"
>
Role
</a>
ルートのリダイレクト
ルートリダイレクトは、あるルートから別のルートへ自動的にユーザーを移動させる仕組み。郵便の転送に似ていて、本来の宛先に届いたものを別の住所に送るイメージ。古いURLの対応、デフォルトのルート設定、アクセス制御の整理などに便利。
リダイレクトの設定方法
ルート設定で redirectTo
プロパティを使うとリダイレクトを定義できる。値は文字列。
import { Routes } from '@angular/router';
const routes: Routes = [
// シンプルなリダイレクト
{ path: 'marketing', redirectTo: 'newsletter' },
// パラメータ付きのリダイレクト
{ path: 'legacy-user/:id', redirectTo: 'users/:id' },
// どのルートにもマッチしなかった場合のリダイレクト
{ path: '**', redirectTo: '/login' }
];
この例では3つのリダイレクトがある
-
/marketing
にアクセスしたら/newsletter
に移動 -
/legacy-user/:id
にアクセスしたら/users/:id
に移動 - どのルートにも当てはまらなかったら
/login
に移動(**
はワイルドカード扱い)
pathMatch
を理解する
pathMatch
プロパティを使うと、Angular が URL をルートにどうマッチさせるかを制御できる。
指定できる値は2つ
-
'full'
: URL 全体が完全に一致した場合のみマッチ -
'prefix'
: URL の先頭部分だけ一致すればマッチ
デフォルトではリダイレクトは 'prefix'
が使われる。
pathMatch: 'prefix'
pathMatch: 'prefix'
はデフォルトで使われる戦略で、リダイレクト時に後続のルートも含めてマッチさせたいときに便利。
export const routes: Routes = [
// これは実際には…
{ path: 'news', redirectTo: 'blog' },
// これと同じ意味(明示的に書いた場合)
{ path: 'news', redirectTo: 'blog', pathMatch: 'prefix' },
];
この場合、news
で始まるルートはすべて blog
にリダイレクトされる。
例:
-
/news
→/blog
-
/news/article
→/blog/article
-
/news/article/:id
→/blog/article/:id
pathMatch: 'full'
pathMatch: 'full'
は、指定したパスに完全一致した場合だけリダイレクトさせたいときに使う。
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
];
この場合、ユーザーがルートURL (''
) にアクセスしたときだけ /dashboard
にリダイレクトされる。
/login
や /about
など他のパスは対象外。
同じように news
の例で full
を使うと
export const routes: Routes = [
{ path: 'news', redirectTo: '/blog', pathMatch: 'full' },
];
-
/news
だけが/blog
にリダイレクトされる -
/news/articles
や/news/articles/1
はリダイレクトされず、そのまま扱われる
条件付きリダイレクト
redirectTo
は文字列だけでなく関数も受け取れる。これを使うと、リダイレクト先を動的に決められる。
ただし関数から参照できるのは ActivatedRouteSnapshot
の一部情報だけで、解決済みのタイトルや遅延ロードされたコンポーネントなどはこの段階では取得できない。
戻り値は通常は文字列や URLTree
だが、Observable
や Promise
を返すこともできる。
import { Routes } from '@angular/router';
import { MenuComponent } from './menu/menu.component';
export const routes: Routes = [
{
path: 'restaurant/:location/menu',
redirectTo: (activatedRouteSnapshot) => {
const location = activatedRouteSnapshot.params['location'];
const currentHour = new Date().getHours();
// クエリパラメータで meal が指定されていたら優先
if (activatedRouteSnapshot.queryParams['meal']) {
return `/restaurant/${location}/menu/${activatedRouteSnapshot.queryParams['meal']}`;
}
// 時間帯によって自動リダイレクト
if (currentHour >= 5 && currentHour < 11) {
return `/restaurant/${location}/menu/breakfast`;
} else if (currentHour >= 11 && currentHour < 17) {
return `/restaurant/${location}/menu/lunch`;
} else {
return `/restaurant/${location}/menu/dinner`;
}
}
},
// 実際のメニューページ
{ path: 'restaurant/:location/menu/breakfast', component: MenuComponent },
{ path: 'restaurant/:location/menu/lunch', component: MenuComponent },
{ path: 'restaurant/:location/menu/dinner', component: MenuComponent },
// デフォルトリダイレクト
{ path: '', redirectTo: '/restaurant/downtown/menu', pathMatch: 'full' }
];
時間帯やクエリパラメータに応じてレストランのメニューへリダイレクトするケース
-
redirectTo
関数はルート解決前に実行される -
params
やqueryParams
は使える - 動的にリダイレクト先を変えたい場合に便利
ガードによるルートアクセス制御
ルートガードは、ユーザーが特定のルートに移動できるかどうか、または離れられるかどうかを制御する関数。チェックポイントのような役割で、特定のルートへのアクセス可否を管理する。よくある用途は認証やアクセス制御。
クライアント側のガードだけにアクセス制御を任せるのは危険。ブラウザで動く JavaScript はユーザーに改ざんされる可能性がある。そのため、本当の認可チェックは必ずサーバー側で行う必要があり、クライアント側のガードは補助的な役割にとどめるべき。
ルートガードを作る
Angular CLI を使うとルートガードを生成できる。
ng generate guard CUSTOM_NAME
実行すると、どのタイプのルートガードにするか選択を求められ、それに応じて CUSTOM_NAME-guard.ts
ファイルが作成される。
ルートガードは手動でも作成可能。Angular プロジェクト内に別の TypeScript ファイルを作り、ファイル名の末尾に -guard.ts
を付けて他のファイルと区別するのが一般的。
ルートガードの戻り値
すべてのルートガードは同じ戻り値の型を持つため、ナビゲーション制御の方法を柔軟に選べる。
戻り値 | 説明 |
---|---|
boolean |
true で移動許可、false でブロック(CanMatch ガードは少し挙動が異なる) |
UrlTree または RedirectCommand
|
移動をブロックする代わりに別のルートへリダイレクト |
Promise<T> または Observable<T>
|
最初に返された値を使い、処理後に自動的に購読解除 |
注意: CanMatch
は false
を返した場合、ナビゲーションを完全にブロックせず、他のマッチするルートを試す。
ルートガードの種類
Angular のルートガードには4種類あり、それぞれ目的が異なる。
CanActivate
CanActivate
ガードは、ユーザーがルートにアクセスできるかを判定するためのガード。認証や権限チェックでよく使われる。
デフォルトで受け取れる引数:
-
route: ActivatedRouteSnapshot
→ アクセスしようとしているルートの情報 -
state: RouterStateSnapshot
→ ルーターの現在の状態
戻り値は通常のガードの型(boolean
、UrlTree
、Observable
、Promise
など)が使える。
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const authService = inject(AuthService);
return authService.isAuthenticated();
};
ユーザーをリダイレクトしたい場合は UrlTree
や RedirectCommand
を返す。
false
を返してからプログラムでナビゲートするのは避ける。
詳しくは API ドキュメントを参照
CanActivateChild
CanActivateChild
ガードは、特定の親ルートの子ルートにユーザーがアクセスできるかを判定するガード。ネストされたルート全体を保護したいときに便利。
- ネストが深くても、すべての子ルートに対して一度ずつ実行される
デフォルトで受け取れる引数:
-
childRoute: ActivatedRouteSnapshot
→ アクセスしようとしている子ルートの「将来のスナップショット」情報 -
state: RouterStateSnapshot
→ ルーターの現在の状態
戻り値は通常のガード型(boolean
、UrlTree
、Observable
、Promise
)が使える。
import { CanActivateChildFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const adminChildGuard: CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
詳しくは API ドキュメントを参照
CanDeactivate
CanDeactivate
ガードは、ユーザーが現在のルートを離れることができるかを判定するガード。未保存のフォームなどからの離脱防止によく使われる。
デフォルトで受け取れる引数:
-
component: T
→ 離脱対象のコンポーネントのインスタンス -
currentRoute: ActivatedRouteSnapshot
→ 現在のルート情報 -
currentState: RouterStateSnapshot
→ 現在のルーター状態 -
nextState: RouterStateSnapshot
→ 次に移動するルーター状態
戻り値は通常のガード型(boolean
、UrlTree
、Observable
、Promise
)が使える。
import { CanDeactivateFn } from '@angular/router';
import { FormComponent } from './form.component';
export const unsavedChangesGuard: CanDeactivateFn<FormComponent> = (
component: FormComponent,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot
) => {
return component.hasUnsavedChanges()
? confirm('You have unsaved changes. Are you sure you want to leave?')
: true;
};
- 未保存データがある場合は確認ダイアログを出す
- データがなければ
true
を返して離脱を許可
詳しくは API ドキュメントを参照
CanMatch
CanMatch
ガードは、ルートがマッチするかどうかを判定するガード。他のガードと違い、false
を返してもナビゲーションを完全にブロックせず、他のマッチするルートを試す。機能フラグや A/B テスト、条件付きルートロードに便利。
デフォルトで受け取れる引数:
-
route: Route
→ 評価中のルート設定 -
segments: UrlSegment[]
→ 前の親ルートで消費されていない URL セグメント
戻り値は通常のガード型(boolean
、UrlTree
、Observable
、Promise
)が使える。
import { CanMatchFn, Route, UrlSegment } from '@angular/router';
import { inject } from '@angular/core';
import { FeatureService } from './feature.service';
export const featureToggleGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => {
const featureService = inject(FeatureService);
return featureService.isFeatureEnabled('newDashboard');
};
同じパスで異なるコンポーネントを条件付きで使うことも可能
const routes: Routes = [
{
path: 'dashboard',
component: AdminDashboard,
canMatch: [adminGuard]
},
{
path: 'dashboard',
component: UserDashboard,
canMatch: [userGuard]
}
];
-
/dashboard
にアクセスしたとき、最初にマッチしたガードのルートが使用される
ルートガードを適用する
ルートガードを作成したら、ルート定義で設定する必要がある。
- ガードは配列で指定でき、1つのルートに複数のガードを適用可能
- 配列内の順番で実行される
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { adminGuard } from './guards/admin.guard';
import { canDeactivateGuard } from './guards/can-deactivate.guard';
import { featureToggleGuard } from './guards/feature-toggle.guard';
const routes: Routes = [
// 基本的な CanActivate - 認証必須
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard]
},
// 複数の CanActivate - 認証かつ管理者権限必須
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard, adminGuard]
},
// CanActivate + CanDeactivate - 未保存チェック付き保護ルート
{
path: 'profile',
component: ProfileComponent,
canActivate: [authGuard],
canDeactivate: [canDeactivateGuard]
},
// CanActivateChild - 子ルート全体を保護
{
path: 'users',
canActivateChild: [authGuard],
children: [
{ path: 'list', component: UserListComponent }, // 保護
{ path: 'detail/:id', component: UserDetailComponent } // 保護
]
},
// CanMatch - 機能フラグに応じて条件付きルート
{
path: 'beta-feature',
component: BetaFeatureComponent,
canMatch: [featureToggleGuard]
},
// 機能無効時のフォールバック
{
path: 'beta-feature',
component: ComingSoonComponent
}
];
- 複数ガードを指定すると左から順に判定される
-
CanDeactivate
は離脱時に実行 -
CanActivateChild
は子ルート全体に適用 -
CanMatch
は条件付きでルートを選択
データリゾルバー
データリゾルバーを使うと、ルート遷移の前に必要なデータを取得できる。コンポーネントは表示時にすでにデータを受け取っているため、ローディング状態を挟まずにスムーズに描画でき、ユーザー体験が向上する。
データリゾルバーとは?
データリゾルバーは ResolveFn
を実装したサービスで、ルートが有効になる前に実行される。API やデータベースなどからデータを取得し、その結果は ActivatedRoute
経由でコンポーネントから利用できる。
データリゾルバーを使用する理由
- 空状態を防ぎ、ロード直後にデータを利用可能にする
- 重要データに対してローディングスピナーを不要にし、UXを向上させる
- ナビゲーション前にデータ取得エラーを処理できる
- レンダリング前に必要データを確保し、一貫性を担保(SSRにも有効)
リゾルバーの作成
リゾルバは ResolveFn
関数として作成し、ActivatedRouteSnapshot
と RouterStateSnapshot
を引数に取る。
import { inject } from '@angular/core';
import { UserStore, SettingsStore } from './user-store';
import type { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
import type { User, Settings } from './types';
export const userResolver: ResolveFn<User> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId);
};
export const settingsResolver: ResolveFn<Settings> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const settingsStore = inject(SettingsStore);
const userId = route.paramMap.get('id')!;
return settingsStore.getUserSettings(userId);
};
ルーティングにリゾルバーを設定する
複数のデータリゾルバーはルート設定の resolve
キーに追加でき、Routes
型がその構造を定義する。
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'user/:id',
component: UserDetail,
resolve: {
user: userResolver,
settings: settingsResolver
}
}
];
コンポーネントで解決済みデータにアクセスする
ActivatedRoute
を使用する
signal
関数を使えば、ActivatedRoute
のスナップショットデータから解決済みデータをコンポーネントで取得できる。
import { Component, inject, computed } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import type { User, Settings } from './types';
@Component({
template: `
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`
})
export class UserDetail {
private route = inject(ActivatedRoute);
private data = toSignal(this.route.data);
user = computed(() => this.data().user as User);
settings = computed(() => this.data().settings as Settings);
}
withComponentInputBinding
を使用する
withComponentInputBinding()
を使うと、解決済みデータをコンポーネントの入力として直接受け取れる。
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
bootstrapApplication(App, {
providers: [
provideRouter(routes, withComponentInputBinding())
]
});
この設定で、input
や input.required
を使い、リゾルバーキーに対応する入力をコンポーネントに定義できる。
import { Component, input } from '@angular/core';
import type { User, Settings } from './types';
@Component({
template: `
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`
})
export class UserDetail {
user = input.required<User>();
settings = input.required<Settings>();
}
ActivatedRoute
の注入が不要で、より優れた型安全性を提供する。
リゾルバーでのエラー処理
リゾルバーでエラー処理をしないと NavigationError
が発生し、ルート遷移が失敗してユーザー体験が損なわれる。
withNavigationErrorHandler
でのエラー処理の一元
withNavigationErrorHandler
を使うと、リゾルバーを含むすべてのナビゲーションエラーを一元的に処理でき、エラー処理ロジックの重複を避けられる。
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withNavigationErrorHandler } from '@angular/router';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { routes } from './app.routes';
bootstrapApplication(App, {
providers: [
provideRouter(routes, withNavigationErrorHandler((error) => {
const router = inject(Router);
if (error?.message) {
console.error('Navigation error occurred:', error.message)
}
router.navigate(['/error']);
}))
]
});
リゾルバーはデータ取得に専念でき、エラーは一元化されたハンドラーで管理される。
export const userResolver: ResolveFn<User> = (route) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId);
};
ルーターイベントのサブスクリプションによるエラー管理
ルーターの NavigationError
を監視することで、リゾルバーのエラーを細かく制御し、カスタム回復ロジックを実装できる。
import { Component, inject, signal } from '@angular/core';
import { Router, NavigationError } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map } from 'rxjs';
@Component({
selector: 'app-root',
template: `
@if (errorMessage()) {
<div class="error-banner">
{{ errorMessage() }}
<button (click)="retryNavigation()">Retry</button>
</div>
}
<router-outlet />
`
})
export class App {
private router = inject(Router);
private lastFailedUrl = signal('');
private navigationErrors = toSignal(
this.router.events.pipe(
filter((event): event is NavigationError => event instanceof NavigationError),
map(event => {
this.lastFailedUrl.set(event.url);
if (event.error) {
console.error('Navigation error', event.error)
}
return 'Navigation failed. Please try again.';
})
),
{ initialValue: '' }
);
errorMessage = this.navigationErrors;
retryNavigation() {
if (this.lastFailedUrl()) {
this.router.navigateByUrl(this.lastFailedUrl());
}
}
}
このアプローチは、次の場合に役立つ。
- ナビゲーション失敗時のカスタム再試行を実装できる
- 失敗タイプに応じて特定のエラーメッセージを表示できる
- ナビゲーション失敗を分析用に追跡できる
リゾルバーでの直接的なエラー処理
エラー発生時にログ記録し、Router
を使って汎用 /users
ページにリダイレクトする userResolver
の例。
import { inject } from '@angular/core';
import { ResolveFn, RedirectCommand, Router } from '@angular/router';
import { catchError, of, EMPTY } from 'rxjs';
import { UserStore } from './user-store';
import type { User } from './types';
export const userResolver: ResolveFn<User | RedirectCommand> = (route) => {
const userStore = inject(UserStore);
const router = inject(Router);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId).pipe(
catchError(error => {
console.error('Failed to load user:', error);
return of(new RedirectCommand(router.parseUrl('/users')));
})
);
};
ナビゲーションの読み込みに関する考慮事項
リゾルバーはロード中の空状態を防ぐが、実行中はナビゲーションがブロックされ、ネットワークリクエストが遅いとルート遷移に遅延が生じる。
ナビゲーションフィードバックの提供
ルーターイベントを監視して、リゾルバー実行中に読み込みインジケーターを表示するとUXが向上する。
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({
selector: 'app-root',
template: `
@if (isNavigating()) {
<div class="loading-bar">Loading...</div>
}
<router-outlet />
`
})
export class App {
private router = inject(Router);
isNavigating = computed(() => !!this.router.currentNavigation());
}
ベストプラクティス
- リゾルバーは必要最低限のデータのみを取得して軽量化
- エラーは必ず適切に処理してユーザー体験を維持
- 解決済みデータのキャッシュでパフォーマンス向上を検討
- データ取得中のナビゲーションブロックに対してローディングインジケーターを表示
- タイムアウトを設定して無限ハングを防ぐ
- 解決データには TypeScript の型を使って型安全を確保
ライフサイクルとイベント
Angularルーターは、ナビゲーション変化に応じてカスタムロジックを実行できるライフサイクルフックとイベントを提供する。
共通ルーターイベント
Angularルーターは、Router.events
Observable 経由で購読可能なナビゲーションイベントを発行し、ナビゲーションやエラーを時系列で追跡できる。
イベント名 | 説明 |
---|---|
NavigationStart |
ナビゲーションが開始され、要求されたURLが指定されたときに発生 |
RoutesRecognized |
ルーターがURLに一致するルートを決定し、ルート状態情報を取得したときに発生 |
GuardsCheckStart |
ルートガード(canActivate, canDeactivateなど)の評価を開始 |
GuardsCheckEnd |
ルートガード評価が完了し、許可/拒否の結果が得られたときに発生 |
ResolveStart |
データ解決フェーズを開始し、リゾルバーがデータ取得を開始 |
ResolveEnd |
データ解決が完了し、必要なすべてのデータが利用可能になったときに発生 |
NavigationEnd |
ナビゲーションが正常に完了し、ルーターがURLを更新したときに発生 |
NavigationSkipped |
ナビゲーションがスキップされたとき(例: 同じURLへの遷移)に発生 |
以下はエラーイベント。
イベント名 | 説明 |
---|---|
NavigationCancel | ルーターがナビゲーションをキャンセルしたときに発生。多くはガードが false を返したことが原因 |
NavigationError | ナビゲーションが失敗したときに発生。無効なルートやリゾルバーエラーが原因になることがある |
ルーターイベントを購読する方法
特定のナビゲーションイベントで処理を実行するには、router.events
を購読してイベントの種類を確認する。
import { Component, inject, signal, effect } from '@angular/core';
import { Event, Router, NavigationStart, NavigationEnd } from '@angular/router';
@Component({ ... })
export class RouterEventsComponent {
private readonly router = inject(Router);
constructor() {
this.router.events.pipe(takeUntilDestroyed()).subscribe((event: Event) => {
if (event instanceof NavigationStart) {
// Navigation starting
console.log('Navigation starting:', event.url);
}
if (event instanceof NavigationEnd) {
// Navigation completed
console.log('Navigation completed:', event.url);
}
});
}
}
@angular/router
の Event
型はグローバルな Event
型とは名前が同じだが、RouterEvent
型とは別物。
ルーティングイベントのデバッグ方法
ルーターイベントの順序が見えないとナビゲーションのデバッグが難しいが、Angularは withDebugTracing()
で全ルーターイベントの詳細なログをコンソールに出力し、問題箇所の特定を支援する。
import { provideRouter, withDebugTracing } from '@angular/router';
const appRoutes: Routes = [];
bootstrapApplication(AppComponent,
{
providers: [
provideRouter(appRoutes, withDebugTracing())
]
}
);
一般的なユースケース
ルーターイベントはアプリケーションで多くの機能に活用でき、いくつかの一般的な利用パターンが存在する。
ローディングインジケーター
ナビゲーション中にローディングインジケーターを表示する。
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-loading',
template: `
@if (loading()) {
<div class="loading-spinner">Loading...</div>
}
`
})
export class AppComponent {
private router = inject(Router);
readonly loading = toSignal(
this.router.events.pipe(
map(() => !!this.router.getCurrentNavigation())
),
{ initialValue: false }
);
}
アナリティクストラッキング
アナリティクス用にページビューを追跡する。
import { Component, inject, signal, effect } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
private router = inject(Router);
private destroyRef = inject(DestroyRef);
startTracking() {
this.router.events.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(event => {
// Track page views when URL changes
if (event instanceof NavigationEnd) {
// Send page view to analytics
this.analytics.trackPageView(url);
}
});
}
private analytics = {
trackPageView: (url: string) => {
console.log('Page view tracked:', url);
}
};
}
エラーハンドリング
ナビゲーションエラーを適切に処理し、ユーザーフィードバックを提供する。
import { Component, inject, signal } from '@angular/core';
import { Router, NavigationStart, NavigationError, NavigationCancel, NavigationCancellationCode } from '@angular/router';
@Component({
selector: 'app-error-handler',
template: `
@if (errorMessage()) {
<div class="error-banner">
{{ errorMessage() }}
<button (click)="dismissError()">Dismiss</button>
</div>
}
`
})
export class ErrorHandlerComponent {
private router = inject(Router);
readonly errorMessage = signal('');
constructor() {
this.router.events.pipe(takeUntilDestroyed()).subscribe(event => {
if (event instanceof NavigationStart) {
this.errorMessage.set('');
} else if (event instanceof NavigationError) {
console.error('Navigation error:', event.error);
this.errorMessage.set('Failed to load page. Please try again.');
} else if (event instanceof NavigationCancel) {
console.warn('Navigation cancelled:', event.reason);
if (event.reason === NavigationCancellationCode.GuardRejected) {
this.errorMessage.set('Access denied. Please check your permissions.');
}
}
});
}
dismissError() {
this.errorMessage.set('');
}
}
すべてのルーターイベント
Angularのルーターイベントをカテゴリー別・発生順に整理した完全リストを参考として示す。
ナビゲーションイベント
これらのイベントは、ルート認識からガードチェック、データ解決までナビゲーションの各フェーズを追跡し、可視性を提供する。
イベント名 | 説明 |
---|---|
NavigationStart |
ナビゲーションが開始されたときに発生 |
RouteConfigLoadStart |
遅延読み込みされるルート設定の読み込み前に発生 |
RouteConfigLoadEnd |
遅延読み込みされたルート設定のロード完了後に発生 |
RoutesRecognized |
URLを解析してルートを認識したときに発生 |
GuardsCheckStart |
ルートガード評価フェーズの開始時に発生 |
GuardsCheckEnd |
ルートガード評価フェーズの終了時に発生 |
ResolveStart |
データ解決フェーズの開始時に発生 |
ResolveEnd |
データ解決フェーズの終了時に発生 |
アクティベーションイベント
これらのイベントは、ルートコンポーネントのインスタンス化・初期化中に発生し、ルートツリー内の親子ルートそれぞれで発生する。
イベント名 | 説明 |
---|---|
ActivationStart |
ルートアクティベーションの開始時に発生 |
ChildActivationStart |
子ルートアクティベーションの開始時に発生 |
ActivationEnd |
ルートアクティベーションの終了時に発生 |
ChildActivationEnd |
子ルートアクティベーションの終了時に発生 |
ナビゲーション完了イベント
これらのイベントはナビゲーションの最終結果を示し、すべてのナビゲーションは成功、キャンセル、失敗、スキップのいずれかで終了する。
イベント名 | 説明 |
---|---|
NavigationEnd |
ナビゲーションが正常に完了したときに発生 |
NavigationCancel |
ナビゲーションがキャンセルされたときに発生 |
NavigationError |
予期しないエラーでナビゲーションが失敗したときに発生 |
NavigationSkipped |
ナビゲーションがスキップされたときに発生(例: 同じURLへの遷移) |
その他のイベント
メインライフサイクル外でも発生する追加イベントがあり、ルーターのイベントシステムに含まれる。
イベント名 | 説明 |
---|---|
Scroll | スクロール中に発生 |
レンダリング戦略
レンダリング戦略とは?
レンダリング戦略は、Angular アプリで HTML がいつどこで生成されるかを決定し、ページロード性能、インタラクティブ性、SEO、サーバー負荷に影響する。
主な戦略
- クライアントサイドレンダリング (CSR): コンテンツをブラウザで完全にレンダリング
- 静的サイト生成 (SSG/Prerendering): ビルド時にコンテンツを事前レンダリング
- サーバーサイドレンダリング (SSR): 初回リクエスト時にサーバーでコンテンツをレンダリング
クライアントサイドレンダリング (CSR)
CSR は Angular のデフォルトで、JavaScript 読み込み後にブラウザ上でコンテンツを完全にレンダリングする。
CSR を使用するタイミング
✅ 適しているケース:
- インタラクティブなアプリ(ダッシュボード、管理パネル)
- リアルタイムアプリケーション
- SEO 不要の社内ツール
- 複雑なクライアントサイド状態を持つSPA
❌ 避けるべきケース:
- SEO が重要な公開コンテンツ
- 初期読み込みパフォーマンスが重視されるページ
CSR のトレードオフ
側面 | 影響 |
---|---|
SEO | 低い - JS 実行までクローラーにコンテンツが表示されない |
初期読み込み | 遅い - 最初に JavaScript をダウンロードして実行する必要がある |
インタラクティブ性 | 読み込み後すぐに利用可能 |
サーバー要件 | 一部設定を除き最小限 |
複雑性 | 最もシンプル - 最小限の設定で動作する |
静的サイト生成 (SSG/プリレンダリング)
SSG はビルド時にページを静的 HTML として生成し、サーバーは初回ロード時にその HTML を返す。ハイドレーション後は SPA 同様にブラウザで完全に動作し、その後のナビゲーションや API 呼び出しはクライアント側で処理される。
ハイドレーションとは、サーバーで事前に生成されたHTMLに対して、クライアント側で JavaScript を「結び付けて」完全な動的アプリとして動作させるプロセス。
SSG を使用するタイミング
✅ 適しているケース:
- マーケティングページやランディングページ
- ブログ記事やドキュメント
- 安定したコンテンツの製品カタログ
- ユーザーごとに変化しないコンテンツ
❌ 避けるべきケース:
- ユーザー固有のコンテンツ
- 頻繁に変化するデータ
- リアルタイム情報
SSG のトレードオフ
側面 | 影響 |
---|---|
SEO | 優れている - 完全なHTMLが即座に利用可能 |
初期ロード | 最速 - 事前生成されたHTMLを配信 |
インタラクティブ性 | ハイドレーション完了後に利用可能 |
サーバー要件 | 提供には不要でCDNに最適 |
ビルド時間 | 長い - すべてのページを事前生成 |
コンテンツ更新 | 再ビルドと再デプロイが必要 |
サーバーサイドレンダリング (SSR)
SSR は初回リクエスト時にサーバーで HTML を生成して SEO に優れた動的コンテンツを提供する。クライアントはハイドレーション後に SPA のように動作し、その後のナビゲーションやAPI呼び出しはクライアント側で処理される。
SSR を使用するタイミング
✅ 適しているケース:
- Eコマースの商品ページ(動的価格や在庫情報)
- ニュースサイトやソーシャルメディアフィード
- 頻繁に変化するパーソナライズコンテンツ
❌ 避けるべきケース:
- 静的コンテンツ(SSGを利用する方が適切)
- サーバーコストを抑えたい場合
SSR のトレードオフ
側面 | 影響 |
---|---|
SEO | 優れている - クローラー向けに完全なHTMLを提供 |
初回ロード | 高速 - コンテンツを即座に表示 |
インタラクティブ性 | ハイドレーション完了まで遅延 |
サーバー要件 | サーバーが必要 |
パーソナライゼーション | ユーザーコンテキストに完全アクセス可能 |
サーバーコスト | 高い - 初回リクエスト時にサーバーでレンダリング必要 |
適切な戦略の選択
決定マトリクス
必要なもの | 推奨戦略 | 理由 |
---|---|---|
SEO + 静的コンテンツ | SSG | 事前レンダリングされたHTMLで最速の読み込み |
SEO + 動的コンテンツ | SSR | 初回リクエストで最新のコンテンツを提供 |
SEOなし + インタラクティブ性 | CSR | 最もシンプルでサーバー不要 |
要件が混在 | Hybrid | ルートごとに異なる戦略を適用可能 |