Angular PR

【Angular】computedで状態管理革命!getterとの5つの違い

【Angular】computedで状態管理革命!getterとの5つの違い
記事内に商品プロモーションを含む場合があります

こんにちは!

Angular 16から導入された「computed」プロパティは、コンポーネントの状態管理を効率的に行うための重要な機能です。

computedプロパティって何?
どうやって使うの?
既存のgetterと何が違うの?

このように疑問に思っている方も多いのではないでしょうか?

この記事では、Angularのcomputedプロパティのメリットから具体的な使い方、パフォーマンス最適化のポイントまで、詳しくご紹介します。

この記事は次のような人におすすめです。

この記事はこんな人におすすめ!
  • Angularでcomputedプロパティを使ってみたい
  • コンポーネントの状態管理をもっと効率的にしたい
  • パフォーマンスを意識した実装方法を知りたい
  • getterとcomputedの違いが分からない

この記事を読めば、Angularのcomputedプロパティがどういうものか分かるだけでなく、具体的な実装方法も理解できるようになりますよ!
さらに、パフォーマンスを最適化するためのコツもお伝えしています。

「Angularでより効率的な開発がしたい方」「パフォーマンスを改善したい方」は、ぜひ参考にしてください。

それでは、順を追って詳しく見ていきましょう!

そもそもcomputedプロパティとは?

まずは、computedプロパティについて簡単におさらいしておきましょう。

computedプロパティとは、Angular 16で導入された新しい機能で、signalから派生した値を効率的にキャッシュする仕組みです。主な特徴として、依存するsignalの値が変更されるまでキャッシュを保持し、不要な再計算を防ぐことができます。

例えば、商品の合計金額を計算する場合を考えてみましょう。以下のような配列があったとします。

items = signal([
  { name: '商品A', price: 100, quantity: 2 },
  { name: '商品B', price: 200, quantity: 1 },
  { name: '商品C', price: 300, quantity: 3 }
]);

この商品リストの合計金額を計算する場合、従来のgetterを使用すると以下のようになります。

get totalPrice() {
  return this.items().reduce((sum, item) => sum + item.price * item.quantity, 0);
}

これに対して、computedプロパティを使用すると以下のように書けます。

totalPrice = computed(() => {
  return this.items().reduce((sum, item) => sum + item.price * item.quantity, 0);
});

一見似ているように見えますが、computedプロパティには大きな利点があります。それは、計算結果がキャッシュされ、依存するsignalの値が変更されるまで再計算されないという点です。これにより、パフォーマンスが大幅に向上する可能性があります。

computedプロパティの具体的な実装方法

それでは、computedプロパティの具体的な実装方法を見ていきましょう。基本的な使い方から、より複雑なケースまで、順を追って説明します。

基本的な使い方

まず、最も基本的な使い方を見てみましょう。computedプロパティを使用するには、@angular/coreからcomputedsignalをインポートする必要があります。

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-product',
  template: `
    <div>
      <h2>商品情報</h2>
      <p>価格: {{ price() }}</p>
      <p>税率: {{ taxRate() }}</p>
      <p>税込価格: {{ priceWithTax() }}</p>
    </div>
  `
})
export class ProductComponent {
  // signalの定義
  price = signal(1000);
  taxRate = signal(0.1);

  // computedプロパティの定義
  priceWithTax = computed(() => {
    return this.price() * (1 + this.taxRate());
  });
}

この例では、pricetaxRateの2つのsignalから、税込価格を計算するcomputedプロパティを作成しています。

複数の依存関係を持つ場合

複数のsignalに依存するcomputedプロパティを実装する方法を見ていきましょう。実際のアプリケーションでは、複数の値を組み合わせて計算することが多いです。

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-shopping-cart',
  template: `
    <div>
      <h2>ショッピングカート</h2>
      <p>小計: {{ subtotal() }}</p>
      <p>送料: {{ shippingFee() }}</p>
      <p>消費税: {{ tax() }}</p>
      <p>合計: {{ total() }}</p>
    </div>
  `
})
export class ShoppingCartComponent {
  // 基本となるsignal
  items = signal([
    { name: '商品A', price: 1000, quantity: 2 },
    { name: '商品B', price: 2000, quantity: 1 }
  ]);
  shippingFee = signal(500);
  taxRate = signal(0.1);

  // 小計の計算
  subtotal = computed(() => {
    return this.items().reduce((sum, item) =>
      sum + item.price * item.quantity, 0);
  });

  // 消費税の計算
  tax = computed(() => {
    return this.subtotal() * this.taxRate();
  });

  // 最終的な合計金額の計算
  total = computed(() => {
    return this.subtotal() + this.shippingFee() + this.tax();
  });
}

この例では、商品リスト、送料、税率という3つの基本となるsignalから、段階的に計算を行っています。特に注目すべき点は以下の通りです。

依存関係の連鎖
  • subtotalitemsに依存
  • taxsubtotaltaxRateに依存
  • totalsubtotalshippingFeetaxに依存
最適化された再計算
  • itemsが変更された場合:subtotaltaxtotalが再計算
  • taxRateが変更された場合:taxtotalのみが再計算
  • shippingFeeが変更された場合:totalのみが再計算

条件分岐を含む実装

computedプロパティ内で条件分岐を使用するケースも多くあります。例えば、注文金額に応じて送料を変更する場合などです。

@Component({
  selector: 'app-dynamic-shipping',
  template: `
    <div>
      <h2>配送料金計算</h2>
      <p>注文金額: {{ orderAmount() }}</p>
      <p>送料: {{ calculatedShippingFee() }}</p>
      <p>合計: {{ totalWithShipping() }}</p>
    </div>
  `
})
export class DynamicShippingComponent {
  orderAmount = signal(5000);
  baseShippingFee = signal(800);
  freeShippingThreshold = signal(10000);

  // 注文金額に応じて送料を計算
  calculatedShippingFee = computed(() => {
    if (this.orderAmount() >= this.freeShippingThreshold()) {
      return 0;  // 無料配送
    }
    return this.baseShippingFee();
  });

  // 合計金額の計算
  totalWithShipping = computed(() => {
    return this.orderAmount() + this.calculatedShippingFee();
  });
}

オブジェクトや配列を扱う実装

複雑なオブジェクトや配列を扱う場合の実装例も見てみましょう。

@Component({
  selector: 'app-order-summary',
  template: `
    <div>
      <h2>注文サマリー</h2>
      <div *ngFor="let item of groupedItems()">
        <p>{{ item.category }}: {{ item.total }}</p>
      </div>
      <p>カテゴリー別合計: {{ categoryTotals() | json }}</p>
    </div>
  `
})
export class OrderSummaryComponent {
  // 商品データ
  items = signal([
    { name: 'A', category: '食品', price: 500, quantity: 2 },
    { name: 'B', category: '食品', price: 800, quantity: 1 },
    { name: 'C', category: '日用品', price: 1200, quantity: 3 }
  ]);

  // カテゴリーごとにグループ化
  groupedItems = computed(() => {
    const groups = new Map<string, number>();

    for (const item of this.items()) {
      const currentTotal = groups.get(item.category) || 0;
      groups.set(
        item.category,
        currentTotal + (item.price * item.quantity)
      );
    }

    return Array.from(groups.entries()).map(([category, total]) => ({
      category,
      total
    }));
  });

  // カテゴリー別の合計を計算
  categoryTotals = computed(() => {
    return this.groupedItems().reduce((acc, item) => {
      acc[item.category] = item.total;
      return acc;
    }, {} as Record<string, number>);
  });
}

このように、computedプロパティは単純な計算から複雑なデータ処理まで、様々なケースで活用できます。次のセクションでは、computedプロパティを使用する際のベストプラクティスについて見ていきましょう。

computedプロパティを使うメリット

computedプロパティを使用することには、実はたくさんのメリットがあります。ここでは、主な5つのメリットについて詳しく解説します。

1. パフォーマンスの向上

computedプロパティは、依存するsignalの値が変更されるまで計算結果をキャッシュします。これにより、複雑な計算を含むプロパティでも、不要な再計算を防ぐことができます。

例えば、大量のデータを扱うリストの並び替えや集計処理などで、特に効果を発揮します。以下は具体的な例です。

// 商品リストを金額順にソートして表示する例
sortedItems = computed(() => {
  console.log('ソート処理実行'); // この処理はsignalの値が変更されるまで実行されない
  return [...this.items()].sort((a, b) => b.price - a.price);
});

2. コードの可読性向上

computedプロパティを使用することで、計算ロジックを明確に分離でき、コードの意図が分かりやすくなります。また、依存関係も明示的になるため、コードの保守性も向上します。

// 税込価格を計算する例
price = signal(1000);
taxRate = signal(0.1);

priceWithTax = computed(() => {
  return this.price() * (1 + this.taxRate());
});

// 配送料込みの合計金額を計算する例
shippingFee = signal(500);
totalWithShipping = computed(() => {
  return this.priceWithTax() + this.shippingFee();
});

3. 自動的な依存関係の追跡

computedプロパティは、依存するsignalを自動的に追跡します。これにより、値の変更があった場合のみ再計算が行われ、開発者が手動で依存関係を管理する必要がありません。

// ユーザー情報の表示例
firstName = signal('John');
lastName = signal('Doe');

fullName = computed(() => {
  return `${this.firstName()} ${this.lastName()}`;
});
// firstNameまたはlastNameのsignalが変更された時のみ再計算される

4. テストの容易さ

computedプロパティは純粋な関数として実装できるため、単体テストが書きやすくなります。また、依存関係が明確なため、テストケースの作成も容易です。

// テストしやすい実装例
class UserComponent {
  firstName = signal('John');
  lastName = signal('Doe');

  fullName = computed(() => {
    return `${this.firstName()} ${this.lastName()}`;
  });
}

// テストコード
it('should combine first and last name', () => {
  const component = new UserComponent();
  expect(component.fullName()).toBe('John Doe');
});

5. デバッグのしやすさ

computedプロパティは、依存関係が明確で、値の変更タイミングが予測しやすいため、デバッグが容易になります。また、Angular DevToolsでの監視も容易です。

// デバッグしやすい実装例
items = signal([
  { name: '商品A', price: 100, quantity: 2 },
  { name: '商品B', price: 200, quantity: 1 }
]);

totalPrice = computed(() => {
  const result = this.items().reduce((sum, item) => {
    console.log(`計算中: ${item.name}`); // デバッグ用のログ
    return sum + item.price * item.quantity;
  }, 0);
  console.log(`合計金額: ${result}`);
  return result;
});

これらのメリットを考えると、computedプロパティを使用することは、多くの場面で有効な選択肢となるでしょう。

computedプロパティとgetterの違い

「computedプロパティとgetterって、結局何が違うの?」という疑問を持つ方も多いでしょう。ここでは、両者の違いを具体的に説明します。

以下の表で、主な違いをまとめてみました。

特徴 computedプロパティ getter
キャッシュ あり(依存するsignalが変更されるまで) なし(毎回計算)
パフォーマンス 優れている(再計算を最小限に抑える) 劣る(アクセスごとに再計算)
メモリ使用量 やや多い(キャッシュを保持) 少ない(キャッシュなし)
使用シーン 複雑な計算や頻繁なアクセス 単純な計算や少ないアクセス
依存関係の追跡 自動(signal 手動

それぞれの実装例を見てみましょう。

// getterを使用した例
class ProductList {
  items = [
    { name: 'A', price: 100, quantity: 2 },
    { name: 'B', price: 200, quantity: 1 }
  ];

  get total() {
    console.log('計算実行'); // アクセスするたびに実行される
    return this.items.reduce((sum, item) =>
      sum + item.price * item.quantity, 0);
  }
}

// computedプロパティを使用した例
class ProductList {
  items = signal([
    { name: 'A', price: 100, quantity: 2 },
    { name: 'B', price: 200, quantity: 1 }
  ]);

  total = computed(() => {
    console.log('計算実行'); // signalの値が変更されるまで実行されない
    return this.items().reduce((sum, item) =>
      sum + item.price * item.quantity, 0);
  });
}

具体的なケースで比較してみましょう。

1. 単純な値の結合

// getter
get fullName() {
  return `${this.firstName} ${this.lastName}`;
}

// computed
firstName = signal('John');
lastName = signal('Doe');
fullName = computed(() => {
  return `${this.firstName()} ${this.lastName()}`;
});

この場合、処理が単純なため、どちらを使用してもパフォーマンスの差はほとんどありません。ただし、computedプロパティを使用する場合は、元となる値もsignalである必要があります。

2. 複雑な計算を含む処理

// getter(非効率)
get filteredAndSortedItems() {
  return this.items
    .filter(item => item.price > 1000)
    .sort((a, b) => b.price - a.price);
}

// computed(効率的)
items = signal([/*...*/]);
filteredAndSortedItems = computed(() => {
  return this.items()
    .filter(item => item.price > 1000)
    .sort((a, b) => b.price - a.price);
});

この場合、computedプロパティを使用することで、不要な再計算を防ぎ、パフォーマンスが向上します。

3. 配列の集計処理

// getter(非効率)
get totalQuantity() {
  return this.items.reduce((sum, item) => sum + item.quantity, 0);
}

// computed(効率的)
items = signal([/*...*/]);
totalQuantity = computed(() => {
  return this.items().reduce((sum, item) => sum + item.quantity, 0);
});

配列の要素数が多い場合、computedプロパティを使用することで、パフォーマンスの向上が期待できます。

これらの違いを理解した上で、適切な使い分けをすることが重要です。一般的に、以下のような基準で選択するとよいでしょう。

computedプロパティを使用するケース
  • 複雑な計算処理がある
  • 頻繁にアクセスされる
  • 依存するsignalの変更が少ない
  • パフォーマンスが重要な場面
  • 元となる値がすでにsignalとして管理されている
getterを使用するケース
  • 単純な値の取得や結合
  • アクセス頻度が低い
  • メモリ使用量を最小限に抑えたい
  • 依存関係が単純
  • signalを使用していない値の計算

computedプロパティのベストプラクティス

computedプロパティを効果的に使用するためのベストプラクティスをご紹介します。これらの原則に従うことで、より保守性が高く、パフォーマンスの良いコードを書くことができます。

1. 純粋関数として実装する

computedプロパティは、与えられた入力(signal)に対して、常に同じ出力を返す純粋関数として実装することが重要です。

// 良い例
price = signal(1000);
quantity = signal(2);
total = computed(() => this.price() * this.quantity());

// 悪い例
total = computed(() => {
  this.someGlobalState = this.price() * this.quantity(); // 副作用がある
  return this.someGlobalState;
});

2. 適切な粒度を保つ

computedプロパティは、適切な粒度で分割することが重要です。大きすぎる計算は小さく分割し、小さすぎる計算は統合することを検討しましょう。

// 良い例(適切な粒度)
subtotal = computed(() => this.items().reduce((sum, item) =>
  sum + item.price * item.quantity, 0));
tax = computed(() => this.subtotal() * this.taxRate());
total = computed(() => this.subtotal() + this.tax());

// 悪い例(粒度が大きすぎる)
total = computed(() => {
  const subtotal = this.items().reduce((sum, item) =>
    sum + item.price * item.quantity, 0);
  const tax = subtotal * this.taxRate();
  const shipping = this.calculateComplexShipping();
  const discount = this.applyVariousDiscounts();
  return subtotal + tax + shipping - discount;
});

3. エラーハンドリングを適切に行う

computedプロパティ内でエラーが発生する可能性がある場合は、適切にハンドリングしましょう。

// 良い例
safeTotal = computed(() => {
  try {
    const result = this.items().reduce((sum, item) => {
      if (typeof item.price !== 'number' || typeof item.quantity !== 'number') {
        throw new Error('Invalid item data');
      }
      return sum + item.price * item.quantity;
    }, 0);
    return result;
  } catch (error) {
    console.error('Error calculating total:', error);
    return 0; // デフォルト値を返す
  }
});

4. パフォーマンスを意識した実装

計算コストが高い処理を含む場合は、不要な再計算を避けるように実装しましょう。

// 良い例(必要な部分のみを再計算)
filteredItems = computed(() => {
  console.log('Filtering items...'); // デバッグ用
  return this.items().filter(item => item.price > this.threshold());
});

sortedItems = computed(() => {
  console.log('Sorting filtered items...'); // デバッグ用
  return [...this.filteredItems()].sort((a, b) => b.price - a.price);
});

// 悪い例(毎回すべての処理を実行)
processedItems = computed(() => {
  console.log('Processing all items...'); // デバッグ用
  return this.items()
    .filter(item => item.price > this.threshold())
    .sort((a, b) => b.price - a.price);
});

5. 適切な命名規則を使用する

computedプロパティには、その計算結果が何を表すのかが明確に分かる名前をつけましょう。

// 良い例
totalPriceWithTax = computed(() => this.subtotal() * (1 + this.taxRate()));
isCheckoutEnabled = computed(() => this.cartItems().length > 0 && this.isUserLoggedIn());
formattedPrice = computed(() => `¥${this.price().toLocaleString()}`);

// 悪い例
calc = computed(() => this.subtotal() * (1 + this.taxRate()));
check = computed(() => this.cartItems().length > 0 && this.isUserLoggedIn());
display = computed(() => `¥${this.price().toLocaleString()}`);

6. テスト可能な設計を心がける

computedプロパティは、単体テストが書きやすい形で実装することが重要です。

@Injectable()
export class PriceCalculatorService {
  private items = signal<CartItem[]>([]);
  private taxRate = signal(0.1);

  // テスト可能な設計
  subtotal = computed(() => {
    return this.calculateSubtotal(this.items());
  });

  // テスト用に分離された純粋な関数
  private calculateSubtotal(items: CartItem[]): number {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}

// テストコード
describe('PriceCalculatorService', () => {
  it('should calculate subtotal correctly', () => {
    const service = new PriceCalculatorService();
    const result = service['calculateSubtotal']([
      { price: 100, quantity: 2 },
      { price: 200, quantity: 1 }
    ]);
    expect(result).toBe(400);
  });
});

キャリア形成/給与還元
ひとつひとつ真摯に向き合う企業
ONE_WEDGE社員募集

株式会社 ONE WEDGEでは、新たな仲間を募集しています!

私たちと一緒に、革新的で充実したキャリアを築きませんか?
当社は、従業員が仕事と私生活のバランスを大切にできるよう、充実した福利厚生を整えています。

  • 完全週休2日制(土日休み)で、祝日や夏季休暇、年末年始休暇もしっかり保証!
  • 様々な休暇制度(有給、慶弔、産前・産後、育児、バースデー休暇)を完備!
  • 従業員の成長と健康を支援するための表彰制度、資格取得支援、健康促進手当など!
  • 生活を支えるテレワーク手当、記事寄稿手当、結婚祝金・出産祝金など、様々な手当を提供!
  • 自己啓発としての書籍購入制度や、メンバー間のコミュニケーションを深める交流費補助!
  • 成果に応じた決算賞与や、リファラル採用手当、AI手当など、頑張りをしっかり評価!
  • ワークライフバランスを重視し、副業もOK!

株式会社 ONE WEDGEでは、一人ひとりの従業員が自己実現できる環境を大切にしています。
共に成長し、刺激を与え合える仲間をお待ちしております。
あなたの能力と熱意を、ぜひ当社で発揮してください。
ご応募お待ちしております!

ホームページ、採用情報は下記ボタンからご確認ください!

応募、ご質問など、LINEでお気軽にご相談ください♪

まとめ

ここまで、Angularのcomputedプロパティについて詳しく解説してきました。改めて、重要なポイントをおさらいしましょう。

  • computedプロパティはsignalから派生した値を効率的に管理できる
  • キャッシュ機能により、パフォーマンスが向上する
  • 依存関係の自動追跡により、コードの保守性が高まる
  • 単純な計算からデータ処理まで、様々なケースで活用できる
  • 適切な設計と実装により、テストがしやすく保守性の高いコードを実現できる
  • getterとの使い分けを理解し、適切な場面で活用することが重要
  • ベストプラクティスに従うことで、より効果的な実装が可能

Angularのcomputedプロパティは、アプリケーションの状態管理をより効率的にするための強力なツールです。signalと組み合わせることで、コードの可読性を高めながら、パフォーマンスも確保することができます。

今回ご紹介した実装方法やベストプラクティスを参考に、ぜひご自身のプロジェクトでも活用してみてください。適切に使用することで、より保守性が高く、パフォーマンスの良いアプリケーションを作ることができるはずです。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です