Skip to content

Angular Signals:linkedSignalとcomputedの使い分け

問題の核心

AngularのSignalsシステムにおいて、computedlinkedSignal はどちらもリアクティブな値の派生に使用されますが、その振る舞いと適切なユースケースには重要な違いがあります。特に以下のようなシナリオで混乱が生じます:

typescript
const shippingOptions = signal(['Ground', 'Air', 'Sea']);

// どちらを使うべき?
const option1 = computed(() => shippingOptions()[0]);
const option2 = linkedSignal(() => shippingOptions()[0]);

両者ともshippingOptionsの変更に反応しますが、linkedSignal にはcomputedにはない重要な特徴があります。両者の本質的な違いを理解することで、より適切な選択が可能になります。

linkedSignalとcomputedの本質的な違い

computedの特徴

  • 読み取り専用の派生シグナル
  • ソースシグナルに基づいて値を計算
  • 外部からの直接的な書き込み不可
  • 純粋な派生状態に適する
typescript
// computedの基本使用例
const totalPrice = computed(() => basePrice() * taxRate());

linkedSignalの特徴

  • 書き込み可能な派生シグナル(WritableSignalの特性を持つ)
  • ソースシグナルから初期値を派生
  • 外部から直接値を更新可能
  • 前回の値にアクセス可能な計算ロジック
  • 派生元と双方向バインディングが必要な場合に適する
typescript
// linkedSignalの基本使用例
const selectedItem = linkedSignal(() => itemList()[0]);

// 書き込み操作が可能
selectedItem.set('New Value');

適切なユースケースと実装例

Case 1: 単純な派生状態(computedが適する)

リストが変更されても選択ロジックが変わらない場合、computedで十分です。

typescript
const animals = signal(['Cat', 'Dog', 'Pig']);
const oldestAnimal = computed(() => animals()[0]);

// リスト変更時に先頭要素を自動更新
addAnimal() {
  animals.update(list => [...list, 'Bird']);
}

TIP

このケースではcomputedが最適
選択ロジックが静的で、外部から状態を変更する必要がない場合はシンプルなcomputedで十分です。

Case 2: 動的選択状態の管理(linkedSignalが適する)

リストが変更されても選択状態を維持したいが、選択対象がリストから削除された場合はリセットしたい場合、linkedSignalが威力を発揮します。

typescript
const animals = signal(['Cat', 'Dog', 'Pig']);

const selectedAnimal = linkedSignal({
  source: animals,
  computation: (currentList, previousSelection) => {
    // 以前の選択値がリストに存在するか確認
    if (previousSelection?.value && 
        currentList.includes(previousSelection.value)) {
      return previousSelection.value;
    }
    return currentList[0]; // 存在しない場合は先頭を選択
  }
});

// 選択を変更する操作
selectNext() {
  const currentIndex = animals().indexOf(selectedAnimal());
  const nextIndex = (currentIndex + 1) % animals().length;
  selectedAnimal.set(animals()[nextIndex]);
}

// リストから要素を削除
removeAnimal(animal: string) {
  animals.update(list => list.filter(a => a !== animal));
  // 削除後にselectedAnimalが自動更新される
}

重要挙動

リストから要素が削除されると:

  1. selectedAnimalは削除前の値を保持しようと試みる
  2. 値がリストに存在しない場合、自動的に先頭要素にリセットされる

Case 3: 双方向バインディングが必要な場合

コンポーネントの入力値に基づいた状態を維持しつつ、コンポーネント内部からも変更したい場合に適しています。

typescript
@Component(...)
export class AnimalSelector {
  // 入力シグナル
  animalOptions = input.required<string[]>();
  
  // 双方向バインディング可能な選択状態
  selectedAnimal = linkedSignal({
    source: this.animalOptions,
    computation: (options, prev) => 
      prev && options.includes(prev.value) ? prev.value : options[0]
  });

  // 内部からの更新
  selectRandomAnimal() {
    const randomIndex = Math.floor(Math.random() * this.animalOptions().length);
    this.selectedAnimal.set(this.animalOptions()[randomIndex]);
  }
}

モデル入力との違い

modelとの違いは、linkedSignalが外部入力と内部状態の自動的な同期ロジックをカスタマイズできる点です。モデルは単純な双方向バインディングですが、linkedSignalでは値の変換や検証ロジックを追加できます。

比較表:computed vs linkedSignal

特徴computedlinkedSignal
書き込み可能×
双方向バインディング×
前回値へのアクセス×
同期ロジックのカスタマイズ制限あり柔軟
パフォーマンス軽量やや重め
ユースケース純粋な派生状態入力と状態の同期が必要な場合

ベストプラクティス

  1. 単純な派生値にはcomputedを選択

    • 読み取り専用で十分な場合
    • 追加の同期ロジックが不要な場合
  2. 以下の条件でlinkedSignalを検討

    • 派生元と双方向の同期が必要な場合
    • リスト変更時に選択状態を自動調整する必要がある場合
    • コンポーネントの入力と内部状態を連動させる必要がある場合
  3. 複雑な同期ロジックの実装

    typescript
    linkedSignal({
      source: dataSource,
      computation: (newData, previousState) => {
        // カスタム同期ロジック
        if (!newData.length) return null;
        if (previousState && newData.some(d => d.id === previousState.id)) {
          return previousState;
        }
        return newData[0];
      }
    });
  4. メモリ管理に注意

    • 大規模なデータセットを扱う場合、不要な依存関係を削除
    • コンポーネント破棄時に自動的にクリーンアップされる

まとめ

Angular SignalsにおけるcomputedlinkedSignalの選択は、状態の更新方向によって決定されます。

  • computed: 単方向データフロー(ソース→派生)
  • linkedSignal: 双方向データフロー(ソース⇔派生)

リスト選択UIや入力値とコンポーネント状態の同期など、複雑な状態連携が必要な場合にlinkedSignalを活用することで、データの整合性を保ちながら直感的な状態管理を実現できます。単純な派生値計算には引き続きcomputedを使用し、適材適所で使い分けることが重要です。