メインコンテンツまでスキップ

Angularルーティング

AngularのルーティングはNext.jsのファイルベースルーティングとは異なり、設定ファイルベースのアプローチを取ります。

1. ルーティングの基本設定​

Angularでは、URLとコンポーネントの対応を ルート定義(Routes 配列) として TypeScript で明示的に記述します。ディレクトリ構造がそのままURLになるわけではありません。

app.routes.ts でのルート定義​

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UserListComponent } from './users/user-list.component';
import { NotFoundComponent } from './not-found.component';

export const routes: Routes = [
{ path: '', component: HomeComponent, title: 'ホーム' },
{ path: 'users', component: UserListComponent, title: 'ユーザー一覧' },
{ path: 'members', redirectTo: 'users', pathMatch: 'full' },
{ path: '**', component: NotFoundComponent }, // 上記のどれにも一致しない場合
];

ポイントは次のとおりです。

  • ルートは 上から順に評価され、最初に一致したものが採用 されます。ワイルドカード ** は必ず最後に置きます。
  • pathMatch: 'full' は、パス全体が一致した場合のみリダイレクトする指定です。空パスのリダイレクトでは必須です。
  • title を指定すると、遷移時に document.title が自動で更新されます。

provideRouter() の設定​

スタンドアロン構成(Angular 15以降)では、app.config.ts で provideRouter() を登録します。RouterModule.forRoot() を使う NgModule 方式は現在では推奨されません。

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
} from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(), // ルートパラメータをinput()にバインド(後述)
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
),
],
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

withXxx() 形式の関数でルーターの機能を必要なものだけ有効化できます(tree-shaking に有利です)。

RouterOutlet コンポーネント​

<router-outlet> は、一致したルートのコンポーネントが描画される場所を示すプレースホルダーです。ルートコンポーネントにはスタンドアロンコンポーネントとして RouterOutlet をインポートします。

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink],
template: `
<header>
<a routerLink="/">ホーム</a>
<a routerLink="/users">ユーザー</a>
</header>
<main>
<router-outlet />
</main>
`,
})
export class AppComponent {}

Next.js との対比​

観点AngularNext.js (App Router)
ルート定義Routes 配列(コードで宣言)app/ ディレクトリ構造(規約)
ページ本体component に指定したクラスpage.tsx
描画位置<router-outlet />children / layout.tsx
404path: '**'not-found.tsx
リダイレクトredirectToredirect() / next.config.js
ページタイトルtitle プロパティmetadata / generateMetadata

Angular は設定が明示的で、URL とファイル配置を独立して設計できます。一方、Next.js はファイルを置くだけでルートが増える手軽さがあります。

2. ナビゲーション​

<a href> の代わりに routerLink を使うと、ページ全体をリロードせずにSPA遷移します。

<!-- 静的パス -->
<a routerLink="/users">ユーザー一覧</a>

<!-- パスパラメータを配列で組み立てる -->
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>

<!-- クエリパラメータ付き -->
<a routerLink="/users" [queryParams]="{ page: 2, sort: 'name' }">2ページ目</a>

routerLinkActive​

現在のURLに一致するリンクへ CSS クラスを付与します。ナビゲーションのハイライトに使います。

<nav>
<a routerLink="/" routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">ホーム</a>
<a routerLink="/users" routerLinkActive="active">ユーザー</a>
</nav>

/ のようなルートリンクは、exact: true を付けないと全URLで active になります。使用するには RouterLinkActive をコンポーネントの imports に追加します。

Router サービスによるプログラマティックナビゲーション​

フォーム送信後など、コードから遷移する場合は Router を inject() で取得します。

import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
selector: 'app-user-form',
template: `<button (click)="save()">保存</button>`,
})
export class UserFormComponent {
private readonly router = inject(Router);

async save(): Promise<void> {
// ...保存処理...
await this.router.navigate(['/users', 42], {
queryParams: { saved: true },
});
// 文字列URLで遷移する場合
// await this.router.navigateByUrl('/users/42?saved=true');
}
}

navigate() は Promise<boolean> を返し、遷移が成功すると true になります(ガードに拒否された場合は false)。

Next.js との比較​

目的AngularNext.js
リンクrouterLink<Link href>
アクティブ判定routerLinkActiveusePathname() で自前判定
コードからの遷移Router.navigate()useRouter().push()
置換遷移navigate([...], { replaceUrl: true })router.replace()

Next.js では <Link> が自動プリフェッチを行いますが、Angular の routerLink は既定ではプリフェッチしません(プリロード戦略は「遅延ロード」の節を参照)。

3. ルートパラメータとクエリパラメータ​

パスパラメータ (:id)​

export const routes: Routes = [
{ path: 'users/:id', component: UserDetailComponent },
];

/users/42 にアクセスすると、id は文字列 '42' として取得できます。

ActivatedRoute サービス​

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';

@Component({
selector: 'app-user-detail',
template: `<h2>User ID: {{ id() }}</h2>`,
})
export class UserDetailComponent {
private readonly route = inject(ActivatedRoute);

// paramMap は Observable。同じコンポーネントでパラメータだけ変わる場合にも追従する
readonly id = toSignal(
this.route.paramMap.pipe(map((p) => p.get('id'))),
{ initialValue: null },
);
}

snapshot.paramMap.get('id') で一度だけ読む方法もありますが、/users/1 から /users/2 へ遷移するとき、Angular は 同じコンポーネントインスタンスを再利用する ため、snapshot では値が更新されません。パラメータの変化に追従したい場合は Observable(paramMap)を購読するか、次の input() バインディングを使います。

withComponentInputBinding による input() バインディング(推奨)​

provideRouter に withComponentInputBinding() を渡すと、パスパラメータ・クエリパラメータ・ルートの data が コンポーネントの input() に直接バインド されます。ActivatedRoute を使わずに済み、最も簡潔です。

import { Component, input, computed } from '@angular/core';

@Component({
selector: 'app-user-detail',
template: `<h2>User {{ userId() }} / page {{ page() }}</h2>`,
})
export class UserDetailComponent {
id = input.required<string>(); // /users/:id
page = input<string>('1'); // ?page=2
userId = computed(() => Number(this.id()));
}

クエリパラメータの読み取り​

// ActivatedRoute を使う場合
const route = inject(ActivatedRoute);
const page$ = route.queryParamMap.pipe(map((q) => q.get('page') ?? '1'));

// 現在のクエリを保ったまま一部だけ更新する
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: 3 },
queryParamsHandling: 'merge',
});

Next.js との対比​

目的AngularNext.js
パスパラメータinput() / ActivatedRoute.paramMapuseParams() / params props
クエリ取得input() / queryParamMapuseSearchParams() / searchParams props
変化への追従Observable / Signal再レンダリング
型基本は string(変換は自前)基本は string

4. ネストされたルートとレイアウト​

子ルートの定義​

children を使うと、親コンポーネントのテンプレート内の <router-outlet> に子ルートが描画されます。共通レイアウトを親に置く用途に最適です。

export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent, // サイドバーなど共通UI
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: AdminUsersComponent },
],
},
];
@Component({
selector: 'app-admin-layout',
imports: [RouterOutlet, RouterLink],
template: `
<aside>
<a routerLink="dashboard">ダッシュボード</a>
<a routerLink="users">ユーザー管理</a>
</aside>
<section>
<router-outlet /> <!-- 子ルートがここに描画される -->
</section>
`,
})
export class AdminLayoutComponent {}

component を持たない コンポーネントレスルート も定義できます。URL のプレフィックスや、ガード・リゾルバをまとめて適用したいときに便利です。

{
path: 'account',
canActivate: [authGuard], // 配下すべてに適用
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'settings', component: SettingsComponent },
],
}

名前付き RouterOutlet​

1つの画面に複数の outlet を置く場合、outlet 名を指定します(補助ルート)。

{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }
<router-outlet /> <!-- プライマリ -->
<router-outlet name="sidebar" /> <!-- 名前付き -->
this.router.navigate([{ outlets: { sidebar: ['chat'] } }]);

URL は /home(sidebar:chat) のようになります。使用頻度は高くなく、モーダルやサイドパネルなど限定的な用途向けです。

Next.js の layout.tsx との比較​

観点AngularNext.js
共通レイアウト親ルートのコンポーネント + <router-outlet />layout.tsx + children
入れ子の表現children 配列フォルダのネスト
ルートグループコンポーネントレスルート(group) フォルダ
並列ルート名前付き outletParallel Routes (@slot)

5. ルートガード​

ガードは、ルートへの遷移を許可・拒否・リダイレクトするための仕組みです。

ガード用途
canActivateルートに入ってよいか
canActivateChild子ルートに入ってよいか
canDeactivateルートから離れてよいか(未保存警告など)
canMatchそのルート定義自体を一致対象にするか(ロール別の出し分けなど)

関数型ガード(新しい書き方)​

Angular 15.2 以降、ガードは 単なる関数 として書くのが推奨です。クラスベースのガード(CanActivate インターフェース)は非推奨となっており、inject() で必要なサービスを取得します。

戻り値は boolean、UrlTree(リダイレクト)、またはそれらの Promise / Observable です。

認証チェック​

// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);

if (auth.isLoggedIn()) {
return true;
}
// ログイン後に元のURLへ戻せるよう returnUrl を渡す
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};

権限チェック(ガードファクトリ)​

引数を取りたい場合は、ガードを返す関数を作ります。

// role.guard.ts
export const roleGuard = (requiredRole: string): CanActivateFn => {
return () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.hasRole(requiredRole)
? true
: router.createUrlTree(['/forbidden']);
};
};
export const routes: Routes = [
{
path: 'admin',
canActivate: [authGuard, roleGuard('admin')],
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];

canDeactivate(未保存変更の警告)​

import { CanDeactivateFn } from '@angular/router';

export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
component.hasUnsavedChanges()
? confirm('保存されていない変更があります。破棄して移動しますか?')
: true;
{ path: 'edit/:id', component: EditComponent, canDeactivate: [unsavedChangesGuard] }

Next.js の middleware.ts との概念比較​

観点Angular ガードNext.js middleware
実行場所ブラウザ(クライアント)。SSR時はサーバーでも実行サーバー / Edge(リクエスト前)
適用単位ルート定義ごとに指定matcher でパスを指定
結果true / false / UrlTreeNextResponse.redirect() など
用途UXのための遷移制御リクエスト単位の認証・書き換え
警告

クライアント側のガードは UX 上の制御であり、セキュリティ境界ではありません。ブラウザ上のコードは改変できるため、認可の最終判断は必ずバックエンド API で行います。

6. 遅延ロード(Lazy Loading)​

初期バンドルを小さくするため、必要になった時点でコードを読み込みます。

loadComponent(単一コンポーネント)​

export const routes: Routes = [
{
path: 'reports',
loadComponent: () =>
import('./reports/reports.component').then((m) => m.ReportsComponent),
},
];

コンポーネントが export default されている場合は、さらに簡潔に書けます。

{ path: 'reports', loadComponent: () => import('./reports/reports.component') }

loadChildren(ルート群)​

機能単位で子ルートの配列ごと遅延ロードします。

// admin/admin.routes.ts
import { Routes } from '@angular/router';

export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminHomeComponent },
{ path: 'users', component: AdminUsersComponent },
];
// app.routes.ts
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
}

動的 import() を境界として、ビルド時にチャンクが自動分割されます。

プリロード戦略​

既定では遷移時に初めてチャンクを取得するため、初回遷移に待ちが発生します。withPreloading() で、アイドル時に先読みできます。

import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

provideRouter(routes, withPreloading(PreloadAllModules));

大規模アプリでは、重要なルートだけを先読みするカスタム戦略(PreloadingStrategy の実装)も検討します。

Next.js との比較​

観点AngularNext.js
分割単位開発者が loadComponent / loadChildren で明示ページ(ルート)ごとに自動
先読み戦略を設定(既定はなし)<Link> が viewport 内で自動プリフェッチ
コンポーネント単位@defer ブロックnext/dynamic / React.lazy

Angular 17以降は、ルート単位に加えて、テンプレート内の @defer ブロックでコンポーネント単位の遅延ロードも可能です。

7. リゾルバ(Resolvers)​

リゾルバは、ルートのアクティブ化前にデータを取得 しておく仕組みです。コンポーネントの表示時にはデータが揃っているため、ローディング表示のちらつきを避けられます。

データのプリフェッチ​

// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService, User } from './user.service';

export const userResolver: ResolveFn<User> = (route) => {
const id = Number(route.paramMap.get('id'));
return inject(UserService).getUserById(id); // Observable / Promise / 値
};
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver },
}

withComponentInputBinding() が有効であれば、解決済みデータは resolve のキー名と同じ input() で受け取れます。

@Component({
selector: 'app-user-detail',
template: `<h2>{{ user().name }}</h2>`,
})
export class UserDetailComponent {
user = input.required<User>();
}

注意点として、リゾルバが完了するまで 遷移が保留 されます。重い処理だとユーザーには「何も起きない」ように見えるため、短時間で終わるデータに限定するか、コンポーネント内で取得してスケルトン表示にする方が適切な場合もあります。リゾルバが失敗(エラー)した場合は遷移がキャンセルされるため、catchError で扱いを決めます。

Next.js との対比​

観点Angular リゾルバNext.js
実行タイミング遷移前(クライアント主体)サーバーでレンダリング時
データの渡し方resolve → input() / ActivatedRoute.dataServer Component が直接 await
旧来の方式-getServerSideProps(Pages Router)
ローディング遷移が保留されるloading.tsx / Suspense でストリーミング

Next.js の Server Components ではコンポーネント自体が非同期にデータを取得するため、リゾルバに相当する専用概念がありません。Angular でも、リゾルバを使わずコンポーネント内でサービスを呼ぶ構成は一般的です。使い分けの目安は「データが無いと画面が成立しない・エラー時は遷移させたくない」場合にリゾルバ、それ以外はコンポーネント内取得です。