👕

今さら聞けないAngularのフック詳説

2024/12/19に公開

おはようございます、こんにちは、こんばんは。@seapolisです。
本記事はAngular Advent Calendar 2024の19日目の記事となります。

https://qiita.com/advent-calendar/2024/angular

私事ではございますが、本記事をもって、Angular Advent Calendarは5年連続の参加となります。
つまり、この間にAngularのバージョンは10も上がったことになりますね。オー怖い。[1]

この5年間でAngularはまるで別物のような進化を遂げ、パラダイムが大きく変化しました。
Standalone Component、Control Flow、Signalsの導入はその最たるものでありますが、そんな中でもあまり変化していないように感じるのが、アプリケーションの初期化やルーティング時のデータ取得等に利用する「フック」であります。

この「フック」は挙動が難解な割に、堅牢なアプリケーションを作るうえでは理解が必須という、初心者殺しの側面を持っています。
というわけで、2025年を迎えようかというこのタイミングで、「Angular最近イケてるらしいし使ってみようかな」と考えている諸氏のため、改めてこの難解なテーマについてまとめてみようというのが本記事の趣旨となります。

お茶でもしばきながらテキトーにご覧ください。[2]

Angularにおけるフック

概ね実行順に

App Initializer

https://angular.dev/api/core/provideAppInitializer

最初にアプリケーションにアクセスした際や、ブラウザをリロードした際の初期化処理に任意の処理を挟むことが出来ます。
返値を持たない同期関数、Promise、Observableを指定できます。

指定した処理が完了しない限り、アプリケーションの初期化が完了せず、真っ白な画面が表示され続けることになりますので、あまり長い時間かかる処理はここで行わないほうが良いでしょう。

実装例

test.initializer.ts
export const testInitializer = async () => {
  // 任意の処理
};
app.config.ts
import {
  ApplicationConfig,
  provideAppInitializer,
  provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { testInitializer } from './test.initializer';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideAppInitializer(testInitializer), // <-- ここ
  ],
};

Guard

https://angular.dev/api/router/CanActivateFn

任意のルートに認可処理を挟むことが出来ます。
この処理はbool値を返す同期関数、PromiseまたはObservableで定義する必要があり、trueの場合はルーティングが続行され、falseの場合はルーティングがキャンセルされるとともに、NavigationCancelイベントを発生させます。

認可に失敗した際に任意のページにリダイレクトしたい場合は、boolではなくUrlTreeというクラスかRedirectCommandというクラスを返す必要があります。
これを行わず、falseを返すだけにしてしまうと、中身のないページが表示されてしまいます。

実装例

test.guard.ts
import { inject } from '@angular/core';
import { RedirectCommand, Router, type CanActivateFn } from '@angular/router';

export const testGuard: CanActivateFn = async (route, state) => {
  const router = inject(Router);

  const isAuthorized = true; // 何らかの判定を行う

  if (!isAuthorized) {
    return new RedirectCommand(router.parseUrl('/'));
  }

  return isAuthorized;
};

app.routes.ts
import { Routes } from '@angular/router';
import { testGuard } from './test.guard';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./top/top.component').then((comp) => comp.TopComponent),
  },
  {
    path: 'test',
    loadComponent: () =>
      import('./test/test.component').then((comp) => comp.TestComponent),
    canActivate: [testGuard], // <-- ここ
  },
];

Resolver

https://angular.dev/api/router/ResolveFn

任意のルートにアクセスされた際、外部からデータを取得するなどの処理を挟むことが出来ます。
この処理は任意の返値を持つ同期関数、PromiseまたはObservableで定義する必要があり、解決されるまでページは表示されません。
ここで定義した返値は、Resolverを指定したルートのコンポーネント内で、ActivatedRouteクラスのdataプロパティから取得することが出来ます。

処理中にエラーが発生した場合は、ルーティングはキャンセルされ、NavigationErrorイベントを発生させます。
もし処理に失敗した際に任意のページにリダイレクトしたい場合は、RedirectCommandというクラスを返す必要があります。
これを行わず、返値を返すだけにしてしまうと、Guardと同様中身のないページが表示されてしまいます。

実装例

test.resolver.ts
import { inject } from '@angular/core';
import { RedirectCommand, Router, type ResolveFn } from '@angular/router';

export const testResolver: ResolveFn<boolean> = async (route, state) => {
  const router = inject(Router);

  try {
    const result = await Promise.resolve(true); // データ取得処理
    return result;
  } catch (error) {
    return new RedirectCommand(router.parseUrl('/'));
  }
};

app.routes.ts
import { Routes } from '@angular/router';
import { testResolver } from './test.resolver';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./top/top.component').then((comp) => comp.TopComponent),
  },
  {
    path: 'test',
    loadComponent: () =>
      import('./test/test.component').then((comp) => comp.TestComponent),
    resolve: {
      test: testResolver, // <-- ここ
    },
  },
];

runGuardsAndResolvers

https://angular.dev/api/router/RunGuardsAndResolvers

フックそのものではありませんが、GuardとResolverの実行戦略を決める上で重要なプロパティなのでここで取り上げます。

初期状態では、RouterLink等でURL内のクエリパラメータが変更された際、GuardやResolverは実行されません。
この挙動を変更したい場合、ルート設定で定義することが出来ます。

  • always
    • ルーティング時は常に処理を実行する
  • paramsChange
    • パスパラメータ、マトリックスパラメータ(カンマから始まる)が変更された際に処理を実行する
  • pathParamsChange
    • パスパラメータが変更された際に処理を実行する
  • paramsOrQueryParamsChange
    • パスパラメータ、マトリックスパラメータ、クエリパラメータが変更された際に処理を実行する
  • pathParamsOrQueryParamsChange
    • パスパラメータ、クエリパラメータが変更された際に処理を実行する

実装例

app.routes.ts
import { Routes } from '@angular/router';
import { testResolver } from './test.resolver';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./top/top.component').then((comp) => comp.TopComponent),
  },
  {
    path: 'test',
    loadComponent: () =>
      import('./test/test.component').then((comp) => comp.TestComponent),
    resolve: {
      test: testResolver,
    },
    runGuardsAndResolvers: 'paramsChange', // <-- ここ デフォルト値:paramsChange
  },
];

イベントの発生順

イベントの発生順

App Initializer → Guard → Resolverの順になっていますね。
子ルートにもGuardやResolverが存在する場合は、親子関係を考慮した実行順になります。

まとめ

Angularには大きくApp Initializer、Guard、Resolverというフックが存在することをご紹介しました。
近年では、UXを向上させるためにコンポーネントライフサイクルの段階[3]でデータを取得するような書き方が奨励されているような気もしますが、これらフックもまだまだ現役だと思いますので、覚えておいて損はないかと思います。

脚注
  1. ちなみに、私はAngular 5からこの道に入りました。IE11対応でヒーコラ言っていた時代が懐かしい ↩︎

  2. お茶をしばく=関西弁で「お茶を飲む」に相当します。 ↩︎

  3. ngOnInit内で処理を実行したり、Deferred Loadingを活用してテンプレート上でasync関数を実行したり ↩︎

Discussion