最終更新日:2020/09/23 原本2020/07/20

新Vue.js「Vue 3」でコンポーネント実装法を大きく変える「Composition API」

JavaScriptフレームワーク「Vue 3」の新機能紹介 第1回

 「Vue 3」は、JavaScriptフレームワーク「Vue.js」の次期バージョンで、2016年にリリースされた「Vue 2」以来のメジャーバージョンアップです。本記事では、Vue 3で導入される新機能のうち、新しいコンポーネント記述形式「Composition API」を説明していきます。

はじめに

 Vue.jsは、Webページのユーザーインタフェース(UI)を構築できるフレームワークです。2015年のバージョン1(Vue 1)、2016年のバージョン2(Vue 2)に続き、バージョン3(Vue 3)が開発中で、2020年第2四半期にリリース予定です。

 本記事では、全2回にわたって、Vue 3の新機能を紹介していきます。今回は、Vue 3で導入されるコンポーネントの新しい記述形式「Composition API」を説明していきます。

対象読者

  • Vue 3の概要を把握したい方
  • 新しいライブラリーを試してみたい方
  • これからVue.jsをプロジェクトに採用する予定の方

必要な環境の準備

 本記事のサンプルコードは、以下の環境で動作を確認しています。

Windows 10 64bit版

  • Node.js v10.18.1 64bit版
  • Vue.js 3.0.0-beta.15
  • Vue CLI 4.4.4
  • Microsoft Edge 83.0.478.54

 サンプルコードを実行するには、サンプルのフォルダーで「npm install」コマンドを実行してライブラリーをダウンロード後、「npm run serve」コマンドを実行して、Webブラウザーで「http://localhost:8080/」を開きます。

Vue 3対応プロジェクトを生成する方法

 CLIツール「Vue CLI」を利用すると、Vue.jsのプロジェクトを生成できます。Vue CLIは、「npm install -g @vue/cli」コマンドでインストールできます。Vue CLIでVue 3対応プロジェクトを生成するには、リスト1のコマンドを実行します。

[リスト1]Vue CLIでVue 3対応プロジェクトを生成するコマンド
vue create <プロジェクト名> # Vue CLIでプロジェクトを生成    ...(1)
cd <プロジェクト名>         # プロジェクトのフォルダーに移動 ...(2)
vue add vue-next            # プロジェクトをVue 3対応        ...(3)

 (1)でプロジェクトを生成します。vue createコマンドではプロジェクトの設定を対話的に選択できますが、本記事ではデフォルト設定(default)を利用します。(2)でプロジェクトのフォルダーに移動後、(3)でプロジェクトをVue 3対応にします。

新しいコンポーネント記述形式「Composition API」

 Composition APIは、Vue.jsにおけるコンポーネントの新しい記述形式です。Vue 2ではプラグインとして提供されていましたが、Vue 3では標準になりました。従来のコンポーネント記述形式(Options API)は、データ/メソッド/算出プロパティの単位で記述するため、複数機能の実装がコードの各所に分散したり、ロジックの再利用が難しかったりしました。Composition APIはこれらの問題を解決します。

 Composition APIの利用法を、図1のサンプルで説明します。このサンプルでは、iOSとAndroidのスマートフォン機種リストを表示します。画面下部のテキストボックスに機種名を入力して「追加」をクリックするとリストに追加されます。iOS/Android/合計の機種数も表示されます。以下ではこのサンプルを複数の方法で実装していきます。

図1 Composition APIの利用法を説明するサンプル
図1 Composition APIの利用法を説明するサンプル

Options APIを利用した実装

 図1のサンプルを、従来のOptions APIで実装した例を、リスト2に示します。

[リスト2]Options APIの実装(p001-options/src/components/Phones.vue)
<template>
  <div>
    <h4>iOS: {{ countIOS }} 機種</h4>
    <ul>
      <li v-for="(elem, index) in iOS" v-bind:key="index">
        {{ elem }}
      </li>
    </ul>
(略:Android機種数とリスト、総機種数)
    <div>
      <input v-model="newIOS" placeholder="iOS機種">
      <button @click="addNewIOS">追加</button>
    </div>
(略:Android機種のテキストボックスと追加ボタン)
  </div>
</template>
<script>
export default {
  // データ ...(1)
  data() {
    return {
      iOS: ['iPhone 11'],         // iOS機種のリスト
      newIOS: '',                 // iOS機種のテキストボックス内容
      android: ['Galaxy S20 5G'], // Android機種のリスト
      newAndroid: ''              // Android機種のテキストボックス内容
    }
  },
  // メソッド ...(2)
  methods: {
    // iOS機種を追加
    addNewIOS() {
      this.iOS.push(this.newIOS)
      this.newIOS = ''
    },
(略:Android機種を追加するaddNewAndroid
  },
  // 算出プロパティ ...(3)
  computed: {
    // iOS機種数
    countIOS() {
      return this.iOS.length
    },
(略:Android機種数を取得するcountAndroid、総機種数を取得するcountTotal
  }
}
</script>

 <template>部で参照するデータは(1)のdataに記述します。ここではiOS、Android機種のリストと、テキストボックス内容を格納する変数を記述しています。追加ボタン押下時に機種リストに追加する処理を行うメソッドは(2)のmethodsに、iOS/Android/総計の機種数を取得する算出プロパティは(3)のcomputedに記述します。

 このようにOptions APIでは、data/methods/computedといった単位で記述するため、iOS関連とAndroid関連の各実装がコード内に分散してしまいます。また、iOSとAndroidの処理は似通っていますが、Options APIの枠組みのため、共通化が難しくなっています。

Composition APIを利用した実装

 リスト2と同じ内容をComposition APIで実装した<script>部を、リスト3に示します。

[リスト3]Composition APIの実装(p002-composition/src/components/Phones.vue)
export default {
  // コンポーネント設定処理 ...(1)
  setup() {
    // reactiveでデータを記述 ...(2)
    const state = reactive({
      iOS: ['iPhone 11'],
      newIOS: '',
      android: ['Galaxy S20 5G'],
      newAndroid: ''
    })
    // メソッド ...(3)
    function addNewIOS() {
      state.iOS.push(state.newIOS)
      state.newIOS = ''
    }
(略:Android機種を追加するaddNewAndroid
    // 算出プロパティ ...(4)
    const countIOS = computed(function() {
      return state.iOS.length
    })
(略:Android機種数を取得するcountAndroid、総機種数を取得するcountTotal
    // ここまで定義してきた内容を返却 ...(5)
    return {
      state, addNewIOS, addNewAndroid, countIOS, countAndroid, countTotal
    }
  }
}

 コンポーネントの設定は(1)のsetupメソッドに記述します。<template>部で参照するデータは(2)の通り、reactiveメソッドの引数にデータ変数を指定して記述します。reactiveメソッドに指定された変数は、メソッドが返却するstate変数のプロパティとなり、値の変更が画面に反映されるようになります。

 メソッド(3)はJavaScriptの標準的な記法で記述します。また、算出プロパティ(4)は、算出処理を実装したfunctionをcomputedメソッドの引数に与えて記述します。メソッドや算出プロパティでデータを参照/操作する場合、stateのプロパティ(「state.<変数名>」)を利用します。(5)で、<template>部で利用するstate/メソッド/算出プロパティをreturnします。

 reactiveメソッドを利用する場合、<template>部ではリスト4の「state.iOS」のように、stateのプロパティでコンポーネントのデータを参照します。

[リスト4]reactive利用時の<template>部での変数参照(p002-composition/src/components/Phones.vue)
<li v-for="(elem, index) in state.iOS" v-bind:key="index">
  {{ elem }}
</li>

refを利用した変数の記述

 reactiveメソッドでは複数の変数をまとめて設定しましたが、リスト5の通り、各変数をrefメソッドで設定する記述方法もあります。

[リスト5]refを利用した記述(p003-ref/src/components/Phones.vue)
setup() {
  // refでデータを記述 ...(1)
  const iOS = ref(['iPhone 11'])
  const newIOS = ref('')
  const android = ref(['Galaxy S20 5G'])
  const newAndroid = ref('')
  // メソッド ...(2)
  function addNewIOS() {
    iOS.value.push(newIOS.value)
    newIOS.value = ''
  }
(略:addNewAndroidメソッド、算出プロパティ)
  // ここまで定義してきた内容を返却 ...(3)
  return {
    iOS, newIOS, android, newAndroid,
(略:メソッド、算出プロパティ)
  }

 (1)で、refメソッドの引数に初期値を与えて各変数を記述します。メソッドや算出プロパティで変数を参照/操作するには、(2)の通り、各変数のvalueプロパティ(「<変数名>.value」)を利用します。最後に(3)で各変数をreturnします。

 reactiveメソッドの場合と異なり、refメソッドを利用する場合、<template>部ではリスト6の「iOS」のように、変数そのものを記述できます(リスト4と比較してください)。

[リスト6]ref利用時の<template>部での変数参照(p003-ref/src/components/Phones.vue)
<li v-for="(elem, index) in iOS" v-bind:key="index">
  {{ elem }}
</li>

reactiveからrefへの変換

 refを利用すれば、<template>部では(stateのプロパティではなく)変数そのものを参照できて記述がシンプルになりますが、<script>部では変数の参照/操作にvalueプロパティが必要となり、また、最後にすべての変数をreturnする必要があるため、変数の数が多い場合は記述が煩雑になります。

 そのため、リスト7の通り、<script>部ではreactiveメソッドでstateを記述しておき、最後に(1)のtoRefsメソッドでrefに変換してreturnする方法がおすすめです。

[リスト7]toRefsメソッドの利用例(p004-torefs/src/components/Phones.vue)
setup() {
  // reactiveでデータを記述
  const state = reactive({
    iOS: ['iPhone 11'],
    newIOS: '',
    android: ['Galaxy S20 5G'],
    newAndroid: ''
  })
(略:メソッド、算出プロパティ)
  // ここまで定義してきた内容を返却
  return {
    ...toRefs(state), // stateの内容をrefに変換 ...(1)
(略:メソッド、算出プロパティ)
  }
}

 この場合、<script>部ではstateを利用しつつ、<template>部ではリスト6の通り変数を直接参照できるようになります。なお、リスト7(1)のtoRefsに付与されている「...」はスプレッド構文と呼ばれ、toRefsメソッドが返却する変数の配列から各要素を取り出すために利用されています。

Composition APIを利用したロジックの共通化

 Composition APIでは、JavaScriptの標準的な記法でデータ/メソッド/算出プロパティなどを記述できるため、ロジックの共通化が容易です。例えば、iOSとAndroidで共通の処理内容を、リスト8の通り切り出せます。リスト8のPhonesLogicメソッドでは、受け取った引数でデータの初期値を設定し、メソッドや算出プロパティと合わせて返却します。

[リスト8]共通ロジック(p005-refactor/src/components/PhonesLogic.js)
import { reactive, computed, toRefs } from 'vue'
export default function PhonesLogic(initialList) {
  // reactiveでデータを記述
  const state = reactive({
    terminals: initialList,
    newTerminal: ''
  })
  // メソッド
  function addNew() {
    state.terminals.push(state.newTerminal)
    state.newTerminal = ''
  }
  // 算出プロパティ
  const count = computed(function () {
    return state.terminals.length
  })
  // ここまで定義してきた内容を返却
  return {
    ...toRefs(state), addNew, count
  }
}

 リスト8の共通ロジックを利用するコンポーネントの実装はリスト9です。(1)でiOS/Androidの各処理を行うロジックを生成し、(2)で総端末数の算出プロパティを定義後、(3)で各ロジックと算出プロパティをまとめてreturnします。

[リスト9]共通ロジックの利用(p005-refactor/src/components/Phones.vue)
setup() {
  // PhoneLogicでiOS/Androidの各処理ロジックを生成 ...(1)
  const iOSLogic = PhonesLogic(['iPhone 11'])
  const androidLogic = PhonesLogic(['Galaxy S20 5G'])
  // 総端末数を計算する算出プロパティ ...(2)
  const countTotal = computed(function() {
    return iOSLogic.count.value + androidLogic.count.value
  })
  // ここまで定義してきた内容を返却 ...(3)
  return {
    iOSLogic, androidLogic, countTotal
  }
}

応用的なトピック:プロパティとライフサイクルフック

 以下では応用的なトピックとして、プロパティとライフサイクルフックをComposition APIで記述する方法を、図2のサンプルで説明します。このサンプルでは、プロパティで機種名/メーカー/重量を設定できるコンポーネントを表示します。コンポーネントがWebページに追加(マウント)された時のライフサイクルフックでログを出力します。

図2 プロパティとライフサイクルフックのサンプル(p006-options2、p007-composition2)
図2 プロパティとライフサイクルフックのサンプル(p006-options2、p007-composition2)

 Options APIで記述したコンポーネントはリスト10です。(1)のpropsで、String型のname(機種名)とvendor(メーカー)、Number型のweight(重量)プロパティを設定します。(2)ではvendorとnameを結合して取得する算出プロパティfullNameを記述していますが、プロパティ値を取得するには(3)の通り「this.<プロパティ名>」とします。(4)はコンポーネントの追加(マウント)時に実行されるmountedライフサイクルフックです。

[リスト10]図2をOptions APIで記述(p006-options2/src/components/OnePhone.vue)
<template>
  <div class="one-phone">
    <h3>{{ fullName }}</h3>
    <div>機種名:{{ name }}</div>
    <div>メーカー:{{ vendor }}</div>
    <div>重量:{{ weight }}g</div>
  </div>
</template>
<script>
export default {
  // プロパティの設定 ...(1)
  props: {
    name: String,
    vendor: String,
    weight: Number
  },
  // 算出プロパティ ...(2)
  computed: {
    fullName() {
      // thisでプロパティを参照 ...(3)
      return this.vendor + ':' + this.name
    }
  },
  // mountedライフサイクルフック ...(4)
  mounted() {
    console.log('コンポーネントがマウントされました')
  }
}
</script>

 同じコンポーネントをComposition APIで記述した<script>部はリスト11となります。<template>部はリスト10と同一です。

[リスト11]図2をComposition APIで記述(p007-composition2/src/components/OnePhone.vue)
export default {
  // プロパティの設定 ...(1)
  props: {
    name: String,
    vendor: String,
    weight: Number
  },
  // setupメソッドの引数propsにプロパティが含まれる ...(2)
  setup(props) {
    // 算出プロパティ ...(3)
    const fullName = computed(function() {
      // propsでプロパティを参照 ...(4)
      return props.vendor + ':' + props.name
    })
    // onMountedライフサイクルフック ...(5)
    onMounted(function() {
      console.log('コンポーネントがマウントされました')
    })
    return {
      fullName
    }
  }
}

 プロパティ設定(1)の記述はリスト10と同一です。setupメソッド(2)の引数propsにプロパティ値が含まれるため、fullName算出プロパティ(3)でプロパティ値を参照するには(4)の通り「props.<プロパティ名>」とします。

 ライフサイクルフックは(5)の通り、処理内容のfunctionをonMountedメソッドの引数に与えて記述します。なおVue 3では、利用できるライフサイクルフックがVue 2から細かく変更されています。詳細はComposition APIのドキュメントを参照してください。

まとめ

 本記事では、Vue.jsの次期バージョン「Vue 3」で導入される新機能のうち、新しいコンポーネント記述形式であるComposition APIについて説明しました。コンポーネント機能の実装が分散したり、再利用が難しかったりするといった、従来のコンポーネント記述形式(Options API)が抱える問題点を、Composition APIを利用して解決できます。Composition APIの利点を生かしてロジックを共通化する例を、記事内で紹介しました。

 次回は、Vue 3に導入されるComposition API以外の新機能を紹介していきます。

参考資料