Vue 3のデフォルト化に合わせて今さらVueのComposition APIを使い始めるにあたって

祝 Vue 3 デフォルト化。

これまで npm install した際やサイト上のドキュメントもすべて Vue 2 ベースのものが基本となっていましたが、これからは Vue 3 の情報が前面に出てきます。しばらくは v3.vuejs.org から構造も変わってドキュメントが見つからなかったりするかもしれないけど、各位頑張れ。

Vue 3 as the New Default | The Vue Point

ということで、Vue の Composition API を使うに当たって例によってあれこれハマったので、主にその点を掘り下げながらメモを起こしていく。

※ Vue 3 のデフォルト化はたまたまです。直前にこんなツイートしてたけど、ほんとにたまたま。

なお、以下は Composition API そのもののよくある紹介や逆に詳細な内部動作の解説が目的ではないので、あえて「Composition API とは何か」という話を避けてメリット、デメリットを挙げていく。

はじめに - Compsition APIへの移行が必要なわけではない

Composition API は React Hooks のように選択肢の一つであり、従来の Options API がなくなるとかいう類の話はないです。安心してよい。

Composition API FAQ | Vue.js

Will Options API be deprecated?

No, we do not have any plan to do so. Options API is an integral part of Vue and the reason many developers love it. We also realize that many of the benefits of Composition API only manifest in larger-scale projects, and Options API remains a solid choice for many low-to-medium-complexity scenarios.

ただし今後はComposition API向けのエコシステムが充実していく

例えば

VueUse

VueUse is a collection of utility functions based on Composition API. We assume you are already familiar with the basic ideas of Composition API before you continue.

ということです。

基本的にはアプリの根幹を担う一部の component (主に root かも)では必要になることが増えるだろうけど、props / emit リレーするだけの多くの component は従来通り Options API で書いて問題なさそう。これは実際に試してみた感想。

実際の UI パーツに関しては template と props / emits と style が大事で、component の組み立てに凝った機能が必要になることはそうそうないので、シンプルな Options API をそのまま使い続けてもよいと思う。というかどっちで書いてもほとんど差がない。

Composition APIのメリット

0. Vue 3でも使わなくてもよいし、Vue 2でも使えるし、Options APIとmixもできる

Composition API は Vue 2 では 2.5 以上と

vuejs/composition-api: Composition API plugin for Vue 2

を組み合わせると利用できる。

逆に Vue 3 化する際に Composition API の利用は必須の要件ではなく、Vue 3 化は plugin の問題を除くと new Vue()createApp() にする程度でなんとかなる。まぁ恐らくかなりのケースで plugin がめちゃくちゃ面倒なことになってるだろうけど。

各 API での component の定義方法はそのままドキュメントを読むと

  • Options API は export default {} で書くやつ
  • Composition API は <script setup> で書くやつ

っぽいけど、

  • setup() を使うと Options API の中に Composition API を書くことができる

ので、この場合、以下のように書くことで Options API で大きな問題となる mixin や expose しすぎ問題を避けるためだけに Composition API を使うことができる。

<script>
expoer default {
  props: {
    ..
  },
  
  mounted () {
    ..
  },

  // この中が Composition API
  setup () {
    // ここで return したものが this や template から参照できる
    // Options API の data() 相当
    return {
      ..      
    }
  },

  computed: {
    ..
  },

  methods: {
    ..
  }
}
</script>

※ TypeScript で使うときは defineComponent() を export した方がよいです。

これは新しく Vue を始める場合はややこしくなるだけなんだけど、v2 以前から始めた人や既存のコードがすでに動いている場合には非常にうまく機能してくれる。

Vue のこういうところがまさに Progressive なんだよなぁと感じるところ。

1. Options APIのmixinの問題を避けることができる

Options API は要は Object の key-value にすべてを押し込めている形なので、mixin も extends も Object をただ mixin するだけ。したがって、どうしても component が依存先の詳細を知って衝突を避ける必要があった。

ざっくり書くとこんな感じ。

{
  mixin: [DependencyComponent],
  methods: {
    func1: function () {
      ..
    },
    // func2 が DependencyComponent で定義されていた場合は壊れる
    func2: function () {
      ..
    }
  }
}

JavaScript の Object をそのまま使おうと思うとこれはどうしようもない。(Vue が component に名前を付けて export する習慣がないのもまずいけど。)

Composition API ではこれを function を compose する形でクリアしようとする。

先ほどの VueUse の例で言うと以下のような感じ。

useCounter | VueUse

import { useCounter } from '@vueuse/core'

export default {
  setup () {
    const { count, inc, dec, set, reset } = useCounter()

    return {
      count
    }
  }
}

v2 時代には count とかそこら中でぶつかりそうな名前は非常にイヤな匂いがしたものだが、Composition API であれば

const { count: clickClount } = useCounter()

のように通常の JavaScript の構文を利用して component 側で衝突を避けることができる。

これが基本的に Object を mix するだけだった Options API ではできなかった芸当。

2. (React Hooksと同様に)thisの問題を避けることができる

Options API はもともとマジカルな this に依存している。このため例えば Options API は method 定義にうっかり arrow function を利用することができない。これは this が変わってしまうため。例えば以下のような arrow function を使ったコードは正常に動作しない。

export default({
  ..
  methods: {
    // OK
    method1 () {
      ..
    },
    // OK
    method2: function () {
      ..
    },
    // NG
    method3: () => {
      ..
    }
  }
  ..  
})

Composition API では this は利用しない ので、this の問題は存在しない。

cf.

3. exposeするものを選べる

上に書いたように Options API は this に依存している。この this からアクセスできるものは template 側からもアクセスできる。

例えばここに破壊的な変更の実行を司る object が含まれている場合、UI 側から安易に好ましくない動作を引き起こすことができるかもしれない。

setup() の return を使うとこれを防ぐことができる。

Options API

export default {
  data () {
    return {
      // ここで god オブジェクトに this からアクセスできるようにすると
      // template からもアクセスできるので、全権が使えてしまう。
      // 例えば destroyWorld() みたいな破壊活動を抑止できない。
      god: new God()
    }
  },
  methods: {
    // 本当はこのメソッドだけを使えるようにしたい
    makeWorld () {
      this.god.makeWorld()
    }
  }
}

Composition API

export default {
  setup () {
    const god = new God()

    const makeWorld = () {
      // このメソッドの中からは god を呼ぶことができる
      god.makeWorld()
    }

    return {
      // template からは makeWorld メソッドしか呼べない
      makeWorld
    }
  }
}

Composition APIのデメリット(混乱ポイント)

従来の Options API であれば多少のインタラクティビティを持たせるために template + α のような使い方をするのはそれほど難しくない。しかし例えば上に挙げた VueUse を使いたいとなると途端に Composition API の書き方やその意味が全部襲いかかってくる。

ハマりやすそうなポイントだけごく簡単に挙げておく。

1. templateから参照する名前とsetup内の実際の値が異なる

export default {
  setup (props) {
    const count = ref(0)

    const inc = () => {
      // count は Proxy であって直接書き換えることはできない
      count.value++
    }

    return {
      // template からは count でアクセスできる
      count,
      inc
    }
  }
}

ref(), reactive() を利用して reactive な値として定義した名前は値ではなく Proxy オブジェクトになり、かつ Options API のように直接値を書き換えても template 側に反映されない。

実際の値は .value になるので、これを書き換える必要がある。

cf. Reactivity API: Core | Vue.js

2. Reactivityの定義はLifecycle Hookの前に済ませておく

import { onBeforeMount } from 'vue'

setup () {
  // OK
  const good = ref('') 
  // NG
  let bad

  onBeforeMount(() => {
    // NG
    bad = ref(0)
  })

  return {
    ..
  }
}

3. Lifecycle Hookの名前が違う

気をつけよう。

Composition API: Lifecycle Hooks | Vue.js

4. setupが長くなるとコードの切れ目が分かりにくい

mounted () {
  ..
},

data () {
  ..
},
..

のようなコードが以下のようになってしまう。

setup () {
  const prop1 = ref(0)

  const method1 = () => {
    ..
  }

  ..

  onMounted(() => {

  })

  ..

  return {
    ..
  }
}

すべて実行なのでシンタックスハイライトで定義部分の切れ目を見つけにくい。ではコードを分割するかというと setup() の中は同じ文脈になっていないといけない。Options API のようにあとから自由に this で参照できるわけではない。mixin の問題が解消された代わりに実際にコードが長くなってしまった場合には普通の JavaScript の知識が必要になる。

この解決方法については

Usecaseを使おう(コードのentry pointからロジックを分離する方法の例) (2022-02-13) | あーありがち

の方にざっくりと書いたので参考になれば幸い。

More