← 解説「提供範囲 — 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'