Dev Study
← 解説「提供範囲 — root提供とコンポーネント提供」に戻る

サンプルコードで身につける: 提供範囲 — root提供とコンポーネント提供

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1providedIn: 'root' はアプリ全体で1個

root 提供の基本を確かめる例です。別々のコンポーネントから inject しても、渡されるのは同じ1つのインスタンスなので、片方で追加したデータをもう片方からも読めます。「アプリ全体の共有の置き場所」として働くのが root 提供です。

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

// アプリ全体で1個だけ作られる
@Injectable({ providedIn: 'root' })
export class FavoriteService {
  items: string[] = [];
  add(item: string) {
    this.items.push(item);
  }
}

@Component({
  selector: 'app-product',
  template: '<button (click)="fav.add(\'りんご\')">お気に入り</button>',
})
export class Product {
  fav = inject(FavoriteService);
}

@Component({
  selector: 'app-favorite-count',
  // 別のコンポーネントでも同じインスタンスが見える
  template: '<p>お気に入り {{ fav.items.length }} 件</p>',
})
export class FavoriteCount {
  fav = inject(FavoriteService);
}

2コンポーネント providers で専用インスタンスを作る

コンポーネントの providers に登録すると、そのコンポーネントのインスタンスごとに専用のサービスが作られます。<app-tab-panel /> を2つ並べれば、それぞれが自分専用の TabState を持ち、互いに影響しません。「部品ごとに独立した状態」がこの1行で実現できます。

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

@Injectable()
export class TabState {
  activeTab = 'info';
  select(tab: string) {
    this.activeTab = tab;
  }
}

@Component({
  selector: 'app-tab-panel',
  // このコンポーネント1つにつき TabState が1つ作られる
  providers: [TabState],
  template:
    '<button (click)="state.select(\'info\')">情報</button>' +
    '<button (click)="state.select(\'review\')">レビュー</button>' +
    '<p>表示中: {{ state.activeTab }}</p>',
})
export class TabPanel {
  state = inject(TabState);
}

// <app-tab-panel /> を2つ並べても、タブの選択は別々に動く

3提供したインスタンスは子ツリーにも届く

コンポーネント提供のインスタンスは、そのコンポーネントの子や孫からも inject できます。届く範囲は「提供したコンポーネントの部品ツリーの中」です。ウィザードの親が状態を提供し、各ステップの子がそれを共有する、といった中間規模の状態共有に向いています。

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

@Injectable()
export class WizardState {
  step = 1;
  next() {
    this.step++;
  }
}

@Component({
  selector: 'app-step-nav',
  // 親が提供したインスタンスを子が受け取る
  template: '<button (click)="state.next()">次へ({{ state.step }})</button>',
})
export class StepNav {
  state = inject(WizardState);
}

@Component({
  selector: 'app-wizard',
  imports: [StepNav],
  // ここで提供すると、この部品ツリー全体で共有される
  providers: [WizardState],
  template: '<h2>ステップ {{ state.step }}</h2><app-step-nav />',
})
export class Wizard {
  state = inject(WizardState);
}

4root提供とコンポーネント提供の寿命の違い

2つの提供方法の寿命を対比する例です。root のサービスは画面を離れても生き続けるためカートの中身が残りますが、コンポーネント提供の下書きは画面を離れると破棄されます。「残したい状態か、画面と一緒に消えてよい状態か」が選択の分かれ目です。

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

// root: 画面を離れてもアプリを閉じるまで生き続ける
@Injectable({ providedIn: 'root' })
export class CartService {
  items: string[] = []; // ページ遷移しても残る
}

// コンポーネント提供用: 画面と運命をともにする
@Injectable()
export class DraftState {
  text = ''; // 画面が破棄されると一緒に消える
}

@Component({
  selector: 'app-review-form',
  providers: [DraftState], // この画面専用の下書き
  template: '<textarea (input)="onInput($event)"></textarea>',
})
export class ReviewForm {
  cart = inject(CartService); // 共有・長生き
  draft = inject(DraftState); // 専用・短命

  onInput(e: Event) {
    this.draft.text = (e.target as HTMLTextAreaElement).value;
  }
}

5root提供に戻すべきサインを見分ける

コンポーネント提供の検索状態を、別画面からも参照したくなった、という設計見直しの例です。部品ツリーの外から必要になった時点で、その状態はコンポーネント提供の守備範囲を超えています。迷ったら root に置き、独立性が必要と分かってから狭める、の順が安全です。

import { Injectable } from '@angular/core';

// Before: 検索パネル専用のつもりでコンポーネント提供にしていた
// @Component({ providers: [SearchState], ... })

// その後「ヘッダーにも検索件数を出したい」という要望が出た。
// ヘッダーは検索パネルの部品ツリーの外なので、
// コンポーネント提供のインスタンスには届かない。

// After: アプリ全体で共有する状態だと分かったので root に変更
@Injectable({ providedIn: 'root' })
export class SearchState {
  keyword = '';
  resultCount = 0;
}

// 判断基準:
// ・1つの部品ツリーに閉じる状態 → コンポーネント providers
// ・複数の画面や部品から見たい状態 → providedIn: 'root'