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

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 にアクセスした場合

  1. '' をチェック → 不一致
  2. 'users/new' をチェック → 一致!ここで処理停止
  3. 'users/:id' は評価されない
  4. 'users' も評価されない
  5. '**' も評価されない

具体的なルートを先に、一般的なルートやワイルドカードを後に置くことが正しいルート定義の基本。

ルートコンポーネントの読み込み戦略

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 }
];

この場合、HomePageLoginPage のコードは初期バンドルに含まれ、アプリ起動時にブラウザがすべてダウンロードして解析する必要がある。

メリット: コンポーネントはすぐに利用可能で、ページ間の遷移がシームレスになる
デメリット: 初期ページ読み込み時の 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 はコンポーネントが破棄されるときに発火
  • attachRouteReuseStrategy がサブツリーをアウトレットに再アタッチするときに発火
  • detachRouteReuseStrategy がサブツリーをアウトレットからデタッチするときに発火

これらのイベントは標準のイベントバインディング構文でリスナーを追加できる。

<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 にアクセスすると、UserRole の両方のリンクに 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 だが、ObservablePromise を返すこともできる。

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 関数はルート解決前に実行される
  • paramsqueryParams は使える
  • 動的にリダイレクト先を変えたい場合に便利

ガードによるルートアクセス制御

ルートガードは、ユーザーが特定のルートに移動できるかどうか、または離れられるかどうかを制御する関数。チェックポイントのような役割で、特定のルートへのアクセス可否を管理する。よくある用途は認証やアクセス制御。

クライアント側のガードだけにアクセス制御を任せるのは危険。ブラウザで動く 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> 最初に返された値を使い、処理後に自動的に購読解除

注意: CanMatchfalse を返した場合、ナビゲーションを完全にブロックせず、他のマッチするルートを試す。

ルートガードの種類

Angular のルートガードには4種類あり、それぞれ目的が異なる。

CanActivate

CanActivate ガードは、ユーザーがルートにアクセスできるかを判定するためのガード。認証や権限チェックでよく使われる。

デフォルトで受け取れる引数:

  • route: ActivatedRouteSnapshot → アクセスしようとしているルートの情報
  • state: RouterStateSnapshot → ルーターの現在の状態

戻り値は通常のガードの型(booleanUrlTreeObservablePromise など)が使える。

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

ユーザーをリダイレクトしたい場合は UrlTreeRedirectCommand を返す。
false を返してからプログラムでナビゲートするのは避ける。

詳しくは API ドキュメントを参照

CanActivateChild

CanActivateChild ガードは、特定の親ルートの子ルートにユーザーがアクセスできるかを判定するガード。ネストされたルート全体を保護したいときに便利。

  • ネストが深くても、すべての子ルートに対して一度ずつ実行される

デフォルトで受け取れる引数:

  • childRoute: ActivatedRouteSnapshot → アクセスしようとしている子ルートの「将来のスナップショット」情報
  • state: RouterStateSnapshot → ルーターの現在の状態

戻り値は通常のガード型(booleanUrlTreeObservablePromise)が使える。

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 → 次に移動するルーター状態

戻り値は通常のガード型(booleanUrlTreeObservablePromise)が使える。

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 セグメント

戻り値は通常のガード型(booleanUrlTreeObservablePromise)が使える。

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 関数として作成し、ActivatedRouteSnapshotRouterStateSnapshot を引数に取る。

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())
  ]
});

この設定で、inputinput.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/routerEvent 型はグローバルな 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 ルートごとに異なる戦略を適用可能
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?