データリゾルバーを使用すると、ルートにナビゲートする前にデータをフェッチでき、コンポーネントが必要なデータをレンダリング前に確実に受け取るようにします。これにより、ローディング状態の必要性を防ぎ、重要なデータを事前に読み込むことでユーザー体験を向上させることができます。
データリゾルバーとは?
データリゾルバーは、ResolveFn関数を実装するサービスです。これは、ルートがアクティブになる前に実行され、API、データベース、またはその他のソースからデータをフェッチできます。解決されたデータは、ActivatedRouteを介してコンポーネントで利用可能になります。
データリゾルバーは、ルートレベルで提供されるサービスと、route引数を介したルート固有の情報にアクセスできます。
データリゾルバーを使用する理由
データリゾルバーは、一般的なルーティングの課題を解決します。
- 空の状態を防ぐ: コンポーネントはロード時にすぐにデータを受け取ります
- より良いユーザー体験: 重要なデータに対してローディングスピナーは表示されません
- エラー処理: ナビゲーション前にデータ取得エラーを処理します
- データの一貫性: レンダリング前に必要なデータが利用可能であることを保証します。これはSSRにとって重要です
リゾルバの作成
リゾルバは、ResolveFn型の関数を記述することで作成します。
これは、ActivatedRouteSnapshotとRouterStateSnapshotをパラメータとして受け取ります。
以下は、inject関数を使用してルートをレンダリングする前にユーザー情報を取得するリゾルバです。
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);
};
ルーティングにリゾルバーを設定する
ルートに1つ以上のデータリゾルバーを追加したい場合は、ルート設定のresolveキーの下に追加できます。Routes型は、ルート設定の構造を定義します。
import {Routes} from '@angular/router';
export const routes: Routes = [
{
path: 'user/:id',
component: UserDetail,
resolve: {
user: userResolver,
settings: settingsResolver,
},
},
];
resolve設定の詳細については、APIドキュメントで確認できます。
コンポーネントで解決済みデータにアクセスする
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を使用する
解決済みデータにアクセスする別のアプローチは、provideRouterでルーターを設定する際に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が発生し、現在のルートへのナビゲーションが失敗し、ユーザーにとって不便な体験につながります。
データリゾルバーでエラーを処理する主な方法は3つあります。
withNavigationErrorHandlerでのエラー処理の一元化- ルーターイベントのサブスクリプションによるエラー管理
- リゾルバーでの直接的なエラー処理
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')!;
// No need for explicit error handling - let it bubble up
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 {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(
map((event) => {
if (event instanceof NavigationError) {
this.lastFailedUrl.set(event.url);
if (event.error) {
console.error('Navigation error', event.error);
}
return 'Navigation failed. Please try again.';
}
return '';
}),
),
{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';
@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());
}
このアプローチにより、リゾルバーがデータをフェッチしている間、ナビゲーションが進行中であることをユーザーが視覚的にフィードバックとして受け取ることができます。
ベストプラクティス
- リゾルバーを軽量に保つ: リゾルバーは、ページが必要とする可能性のあるすべてのデータではなく、必要不可欠なデータのみをフェッチするようにしてください
- エラーを処理する: ユーザーに可能な限り最高の体験を提供するために、常にエラーを適切に処理することを忘れないでください
- キャッシュを使用する: パフォーマンスを向上させるために、解決されたデータをキャッシュすることを検討してください
- ナビゲーションUXを考慮する: データフェッチ中にナビゲーションがブロックされるため、リゾルバー実行中にローディングインジケーターを実装してください
- 適切なタイムアウトを設定する: 無期限にハングしてナビゲーションをブロックする可能性のあるリゾルバーは避けてください
- 型安全性: 解決されたデータにはTypeScriptインターフェースを使用してください
子リゾルバーで親の解決済みデータを読み取る
リゾルバーは親から子へと実行されます。親ルートがリゾルバーを定義している場合、その解決済みデータは、後で実行される子リゾルバーで利用可能になります。
import { inject } from '@angular/core';
import { provideRouter , ActivatedRouteSnapshot } from '@angular/router';
import { userResolver } from './resolvers';
import { UserPosts } from './pages';
import { PostService } from './services',
import type { User } from './types';
provideRouter([
{
path: 'users/:id',
resolve: { user: userResolver }, // user resolver in the parent route
children: [
{
path: 'posts',
component: UserPosts,
// route.data.user is available here while this resolver runs
resolve: {
posts: (route: ActivatedRouteSnapshot) => {
const postService = inject(PostService);
const user = route.parent?.data['user'] as User; // parent data
const userId = user.id;
return postService.getPostByUser(userId);
},
},
},
],
},
]);