はじめに
Vue2からVue3に変わるとともに、Composition APIの導入、ライフサイクルの変更、v-modelの仕様変更など、構成や記述が大きく変更されました。
この記事ではComposition APIの導入について触れ、Vue3で何が変わったか紹介します。
まずはComposition APIの記述を見てみよう
options APIとは違い、Compotion APIでは今までのdataやmethods, computed, ライフサイクルフックはすべてsetup関数内で定義します。
dataはreactiveもしくはrefを使い、methodはfunctionで定義します。
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
setup() {
// refを使う場合は、 const count = ref(0)という記述の仕方になる
const state = reactive({
count: 0, // data: { count: 0 } と同じ
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
なぜ、Composition APIが誕生したの?
Composition API誕生の動機
Vueの作成者Evan You氏いわく、Composition APIを提案する背景となった動機について次のように説明しています。
論理合成(logic composition)は、プロジェクトをスケールアップする場合には、おそらく最も深刻な問題のひとつです。(…)さまざまなタイプのプロジェクトを扱うユーザは、さまざまなニーズに直面します。その中には、オブジェクトベースのAPIを使用して簡単に処理できるものと、できないものがあります。
主な例としては、
- 複数の論理タスクをカプセル化した、大規模な(数百行の)コンポーネント
- 複数のコンポーネント間においてタスクにロジックを共有するニーズ
vueは今日では様々な複雑さを抱えて幅広いプロジェクトに使用されているが、Options APIでは上記1と2のようなケースには簡単には対応できないとのこと
1. 複数の論理タスクをカプセル化した、大規模な(数百行の)コンポーネントについて
例えば、数百行のvueファイル内でデータ取得、取得データをプロパティに入れたり、データ件数の表示などを行う処理を確認するとしたら、data, mounted, method, computedなどを辿る必要があります。
(ソースコードを事前に知っていたとしても)プロジェクトがスケールすればするほどプロパティを見逃したりする可能性があります。
2. 複数のコンポーネント間においてタスクにロジックを共有するニーズについて
Options APIではmixinでコンポーネントを分割できたが、mixinを使用する上で2つの問題があったようです。
- Mixinを使用する側はMixinにどのようなメソッドがあるかわからない
export default Vue.extend({
// mixinを利用する側はIsTouchDeviceに何があるか一見してわからない
mixins: [IsTouchDevice],
data() {...}
- Mixin内でしか使わないメソッドなどをprivateにできない
Vue.jsスタイルガイドではプロパティ・メソッド名の衝突を避けるため、$_
プレフィックス付きの名前にすることを推奨しています
var myGreatMixin = {
// ...
methods: {
$_myGreatMixin_update: function () {
// ...
}
}
}
この2つの問題によりvueのmixinはソースコードの可読性やメンテンス性を下げる可能性があります。
ロジックの抽出と再利用性
The primary advantage of Composition API is that it enables clean, efficient logic reuse in the form of Composable functions. It solves all the drawbacks of mixins, the primary logic reuse mechanism for Options API.
「Composition API誕生の動機」でも記載したが、プロジェクトが大きくなったりすると、vueファイルの記述量が多くなるし、mixinsを使うとしても問題点があります。
Composition APIでのコード分割(Code Organization)のやり方を以下の記事で具体的に説明してくれています。
仮にOptions APIでコード分割するとなったらどうなるでしょう。
thisへの依存やdata、computed、method等を分割元と分割先のソースで記述しないといけないし、正常に動作するとは限らない。
(Composition APIが絶対とは言えませんが、少なくともコード分割と合成のしやすさはOptions APIよりかは勝ると思います)
Vue3の変更点(一部抜粋)
コンポーネントの作成 -defineComponent-
Vue2ではexport default Vue.extend({})にdata, methods, computedを書いていたが、Vue3からはdefineComponentのsetup関数内に記述していく
<template>
...
</template>
<script lang="ts">
import { defineComponent } from "@vue/composition-api";
export default defineComponent({
setup() {
// ここにリアクティブなデータ、関数を定義
const count = ref(0)
}
})
</script>
なお、vue3.2から「script setup構文」が使えるようになったので、defineComponentがいらなくなり、下記のメリットを享受できるようです。
- ポイラープレートが減りより簡潔になる
- props と emit を定義する際に純粋な TypeScript の構文が使える
- ランタイムのパフォーマンスが向上する
- IDE のパフォーマンスが向上する
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
</script>
<template>
<h1>{{ count }}</h1>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</template>
ライフサイクルフック
setupがbeforeCreate、createdのライフサイクルで実行されるため、beforeCreateとcreatedはなくなりました。
メソッド名はすべて「on」を接頭辞としてつけ、setup内にメソッドを記述します。
Options API | Composition API |
---|---|
beforeCreate | 不要 |
created | 不要 |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
<script setup>
import { ref, onMounted } from 'vue'
const el = ref()
onMounted(() => {
el.value
})
</script>
<template>
<div ref="el"></div>
</template>
Fragments
Vue2ではtemplate配下に複数のコンポーネントを配置するとエラーになりdivタグで囲む手段をとっていたが、Vue3では複数のコンポーネント配置(Fragments)ができるようになった
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
emitsオプション
Vue2では親が受け取るプロパティ($emit("update:message")みたいに)を定義できましたが、発行できるイベントの定義ができませんでした。
Vue3では発行するイベントをemitsで定義します。
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
Vue3.2のsetup scriptから、propsはdefineProps、emitsはdefineEmitsで定義するようです。
<script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
<script>
v-model
Vue2では、親子間の双方向バインディングで.sync修飾子をv-bindで利用していました。これが、
<ChildComponent :title.sync="pageTitle" />
<!-- これは下記の省略形です -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
Vue3からはv-modelに引数を渡して変更できるようになります。
<ChildComponent v-model:title="pageTitle" />
<!-- これは下記の省略形です -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
providerパターン
providerパターンとは、provide/inject APIを使用し、Composition APIで切り出した「状態やロジック」をコンポーネント間で共有できるようにする手法です。
provide/inject自体はVue2からあったのですが、あんまり使われる機会がなかったのかなと思います。
(1つのVueファイル大きかったり、mixinsで共通化があったからかな)
providerを使うことで親コンポーネントから子コンポーネントへ簡単に「状態やロジック」を渡せるようになり、今まで親コンポーネントから孫コンポーネントまでのバケツリレーもこれで解消できるようになります。
おわりに
Vue3、Composition API、Options APIからの変更点について触れてきました。
Vu3を使って新しく画面の実装をする場合やVue2からVue3への移行を考えるとなると、どんなコンポーネントの切り方をしてどのコンポーネントのどのロジックを共有できるようにするか等、設計から考える必要がありそう。
デザインを見てAtomic Designを考えるのもいいですが、モジュールの最小単位を「依存」とするAtomic Redesignで考えるのもありかなと思いました。
以下の記事、参考にさせていただきました。ありがとうございます!
参考記事