Vue.jsにおけるcomputedとwatchの違い

2025-07-29

はじめに

Vue.js における methodscomputedwatch はすべて「状態(データ)をどのように扱うか・反応するか」を定義するための仕組みです。

methodsとの違いの明確化

まずは前回の内容であるmethodsとの違いを明確にしましょう。

export default {
  data() {
    return {
      price: 100,
      quantity: 2
    }
  },
  methods: {
    calculateTotal() {
      return this.price * this.quantity;
    }
  },
  computed: {
    total() {
      return this.price * this.quantity;
    }
  },
  watch: {
    price(newVal, oldVal) {
      console.log(`価格が${oldVal}から${newVal}に変更されました`);
    }
  }
}

methodsの特徴

  • 呼び出されるたびに実行:テンプレートで参照される度に計算
  • 明示的な呼び出しが必要:イベントハンドリングや手動での呼び出し
  • 引数を取れる:動的な値を渡せる

computedの特徴

  • 依存関係に基づくキャッシュ:依存するデータが変更された時のみ再計算
  • プロパティのように使用:テンプレートで直接参照可能
  • 引数不可:ゲッター関数として動作

watchの特徴

  • 変更監視:特定のデータの変化を監視
  • 副作用の実行:データ変更時に非同期処理や複雑なロジックを実行
  • 新旧値の取得:変更前後の値にアクセス可能
オプション主な役割タイミング特徴
methods関数として呼び出す処理明示的に呼び出されたときデータを変更したり処理したりするための通常の関数
computed計算結果を自動で返す依存するデータが変化したときに自動再評価キャッシュ付きのリアクティブ計算プロパティ。表示用などに最適
watch値の変化を監視して処理を実行監視対象のデータが変わった瞬間に実行される副作用API呼び出しやログ出力など、処理をトリガーさせたいときに使う

computedの詳細解説

computedプロパティは依存するデータに基づいて計算された値を返します。リアクティブな依存関係を自動的に追跡し、依存するデータが変更された時のみ再計算します。

基本的な使用例

export default {
  data() {
    return {
      firstName: '太郎',
      lastName: '山田'
    }
  },
  computed: {
    fullName() {
      return `${this.lastName} ${this.firstName}`;
    }
  }
}
<template>
  <p>氏名: {{ fullName }}</p>
</template>

セッター付きcomputedプロパティ

computedプロパティはゲッターだけでなくセッターも定義できます。

export default {
  data() {
    return {
      firstName: '太郎',
      lastName: '山田'
    }
  },
  computed: {
    fullName: {
      get() {
        return `${this.lastName} ${this.firstName}`;
      },
      set(newValue) {
        const names = newValue.split(' ');
        this.lastName = names[0];
        this.firstName = names[1] || '';
      }
    }
  }
}

複雑な計算例

export default {
  data() {
    return {
      items: [
        { id: 1, name: '商品A', price: 1000, stock: 5 },
        { id: 2, name: '商品B', price: 2000, stock: 3 },
        { id: 3, name: '商品C', price: 1500, stock: 0 }
      ],
      discountRate: 0.1
    }
  },
  computed: {
    availableItems() {
      return this.items.filter(item => item.stock > 0);
    },
    totalPrice() {
      return this.availableItems.reduce((sum, item) => sum + item.price, 0);
    },
    discountedPrice() {
      return this.totalPrice * (1 - this.discountRate);
    }
  }
}

watchの詳細解説

watchは特定のデータソースを監視し、変更があった時にコールバック関数を実行します。非同期処理や複雑なロジックを実行するのに適しています。

基本的な使用例

export default {
  data() {
    return {
      question: '',
      answer: '質問を入力してください...'
    }
  },
  watch: {
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer();
      }
    }
  },
  methods: {
    async getAnswer() {
      this.answer = '考え中...';
      try {
        const res = await fetch('https://yesno.wtf/api');
        const data = await res.json();
        this.answer = data.answer;
      } catch (error) {
        this.answer = 'エラーが発生しました: ' + error;
      }
    }
  }
}

ディープウォッチ

オブジェクトや配列のネストされたプロパティを監視するにはdeepオプションを使用します。

export default {
  data() {
    return {
      user: {
        name: '太郎',
        preferences: {
          theme: 'light',
          notifications: true
        }
      }
    }
  },
  watch: {
    user: {
      handler(newVal, oldVal) {
        console.log('ユーザー情報が変更されました');
      },
      deep: true
    }
  }
}

即時実行

immediateオプションで作成時にすぐにハンドラを実行できます。

export default {
  data() {
    return {
      apiKey: localStorage.getItem('apiKey') || ''
    }
  },
  watch: {
    apiKey: {
      handler(newVal) {
        localStorage.setItem('apiKey', newVal);
      },
      immediate: true
    }
  }
}

computed vs watch の使い分け

computedを使うべきケース

  • 派生データを表示するため
  • 複数のデータに依存する計算が必要な場合
  • キャッシュによるパフォーマンス向上が必要な場合
export default {
  data() {
    return {
      width: 0,
      height: 0
    }
  },
  computed: {
    area() {
      return this.width * this.height;
    }
  }
}

watchを使うべきケース

  • データ変更時に非同期処理を実行する場合
  • データ変更時に副作用を実行する必要がある場合
  • 変更前後の値に基づいて処理を行う場合
export default {
  data() {
    return {
      searchQuery: '',
      searchResults: []
    }
  },
  watch: {
    searchQuery(newVal) {
      if (newVal.length > 2) {
        this.debouncedSearch();
      }
    }
  },
  created() {
    this.debouncedSearch = _.debounce(this.doSearch, 500);
  },
  methods: {
    async doSearch() {
      const response = await fetch(`/api/search?q=${this.searchQuery}`);
      this.searchResults = await response.json();
    }
  }
}

パフォーマンス比較

computedの利点

  • 依存関係が変わらない限り再計算されない
  • 複数の依存関係を効率的に管理
  • テンプレートがシンプルになる

watchの利点

  • 変更時の具体的な動作を定義可能
  • 非同期処理を扱える
  • 変更前後の値にアクセス可能

実践的なユースケース

フォームバリデーション(computed使用)

export default {
  data() {
    return {
      email: '',
      password: '',
      passwordConfirmation: ''
    }
  },
  computed: {
    emailError() {
      if (!this.email) return 'メールアドレスは必須です';
      if (!this.email.includes('@')) return '有効なメールアドレスを入力してください';
      return '';
    },
    passwordError() {
      if (!this.password) return 'パスワードは必須です';
      if (this.password.length < 8) return 'パスワードは8文字以上必要です';
      return '';
    },
    passwordMatchError() {
      if (this.password !== this.passwordConfirmation) {
        return 'パスワードが一致しません';
      }
      return '';
    },
    isValid() {
      return !this.emailError && !this.passwordError && !this.passwordMatchError;
    }
  }
}

APIポーリング(watch使用)

export default {
  data() {
    return {
      status: 'idle',
      pollingInterval: null,
      lastUpdated: null
    }
  },
  watch: {
    status(newVal) {
      if (newVal === 'active' && !this.pollingInterval) {
        this.startPolling();
      } else if (newVal !== 'active' && this.pollingInterval) {
        this.stopPolling();
      }
    }
  },
  methods: {
    startPolling() {
      this.pollingInterval = setInterval(() => {
        this.fetchStatus();
      }, 5000);
      this.fetchStatus();
    },
    stopPolling() {
      clearInterval(this.pollingInterval);
      this.pollingInterval = null;
    },
    async fetchStatus() {
      const response = await fetch('/api/status');
      const data = await response.json();
      this.lastUpdated = data.timestamp;
    }
  },
  beforeDestroy() {
    this.stopPolling();
  }
}

よくある間違いとベストプラクティス

避けるべきパターン

export default {
  data() {
    return {
      items: [],
      filteredItems: [] // ← データとcomputedの混合は避ける
    }
  },
  watch: {
    items() {
      this.filteredItems = this.items.filter(item => item.active);
      // computedプロパティを使用する方が適切
    }
  }
}

推奨パターン

export default {
  data() {
    return {
      items: []
    }
  },
  computed: {
    filteredItems() {
      return this.items.filter(item => item.active);
    }
  }
}

watchの過剰使用

export default {
  data() {
    return {
      width: 0,
      height: 0,
      area: 0 // ← computedで計算すべき
    }
  },
  watch: {
    width() {
      this.area = this.width * this.height;
    },
    height() {
      this.area = this.width * this.height;
    }
  }
}

methodscomputedwatchの3つの主要なオプションの違いを表で示します。(再掲)

オプション主な役割タイミング特徴
methods関数として呼び出す処理明示的に呼び出されたときデータを変更したり処理したりするための通常の関数
computed計算結果を自動で返す依存するデータが変化したときに自動再評価キャッシュ付きのリアクティブ計算プロパティ。表示用などに最適
watch値の変化を監視して処理を実行監視対象のデータが変わった瞬間に実行される副作用API呼び出しやログ出力など、処理をトリガーさせたいときに使う

まとめ

Vue.jsのcomputedwatchはどちらもリアクティブなシステムの重要な部分ですが、適切な使い分けが重要です。

  • computed:派生データの計算、テンプレート内での複雑な式の簡素化、複数の依存関係を持つ値の効率的な管理に使用
  • watch:データ変更時の副作用の実行、非同期処理、変更前後の値に基づく処理、パフォーマンスが重要な操作(デバウンスなど)に使用

methodsとの主な違いは、methodsが明示的に呼び出される関数であるのに対し、computedは依存関係に基づいて自動的に計算されるプロパティ、watchはデータ変更を監視するためのフックである点です。

適切に使い分けることで、より効率的でメンテナンス性の高いVue.jsアプリケーションを構築できます。