Vue 3.0のComposition APIとはなんだったのか(RC版)
仕事とか趣味とかでComposition APIとだいたい半年くらい向き合ったので、いまのところわかっていることをまとめておきます。 Composition APIを導入する前に読んでおくと、低確率で幸せになれます。 とはいえ高々半年程度で得た知見にすぎないので、もう2ヶ月くらいたつとここに書かれていることが嘘になるかもしれません。
Composition APIが何かという話は他所でたくさん書かれているのでここでは説明しませんが、 この文章を読む前に軽く調べておいたほうがいいと思います。
Composition APIが解決する問題
もうしばらくVue2.xに触れていないので自信がありませんが、僕の記憶が間違ってなければComposition APIは従来の記法と比べて以下のような観点で優れているといえます。
データとそれに対する操作をまとめてかける
Composition APIの登場により、上の方(data)で定義した状態を下の方(methodsとかcomputedとか)で参照・操作している状況が解消できるようになります。 「データとそれに対する処理はなるべく近くに書く」という職業プログラマーがかなり初期に学ぶであろう美しいコードを書くための第一歩がようやく守れるようになります。
また従来の記法では、mountedをはじめとするライフサイクルフックは1つのコンポーネントにつき1つしか記述できなかったため、 複数の異なる処理を同一のライフサイクルフックで行うケースでかなりコードが散らかりがちでした。 Composition APIではsetup()内で同じライフサイクルフックを何回利用しても良く、データと処理の関連をまとめてあげることができます。
テンプレートで必要になるデータや処理だけを公開できる
従来の記法では、dataで定義したコンポーネントの状態やmethodsとかcomputedで定義した処理は有無を言わさずテンプレート部に公開されてしまいます。 疎結合・高凝集を実現するための基本的なテクニックである「公開するインタフェースを最小にする」ことと真っ向から対立しています。
Composition APIを使うと、テンプレートで必要なものに限定して公開することができるようになります。 これにより、テンプレートと処理の間に意図しない依存関係が生まれることを防ぐことができます。
あまり良い例ではありませんが1、以下に連打を防止するために処理中の状態を内部的に持つボタンコンポーネントのサンプルを示します。
<template>
<button @click="submit"><slot></slot></button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
// この例ではリアクティブにする必要すらありませんね...
const processing = ref<boolean>(false)
const submit = () => {
if(processing.value) {
return
}
processing.value = true
// なんらかの処理......
processing.value = false
}
return {
submit
}
}
})
</script>
多重サブミットを防ぐための状態を表すprocessingは外部で利用しないため、公開する必要はありません。 テンプレートで必要となるsubmitだけをsetupの返り値とします。
コンポーネントが関心を持つべきでない処理を分離できる
Vueの単一ファイルコンポーネントには見た目に関する部分(templateとstyle)と機能に関する部分(script)が同居しています。 従来の記法では、コンポーネントの分割にあたっていずれかのコンポーネントに見た目と機能をいいかんじにうまく押し込める必要がありました。 見た目による分割をすべきか機能による分割をすべきか、方針の統一をすることが非常に困難でした。
その結果、見た目に寄ったコンポーネントが作られる一方で、機能を詰め込んだコンポーネントが同じプロジェクト内に作られることが少なくありませんでした。 全体を見渡すとコンポーネントの責務の境界がぼやけてしまっていることがよく起こってます。
Vue 3.0以降では、Composition APIを利用することでロジックをコンポーネントの外に切り出すことができるようになったため、 コンポーネントは見た目をベースにして分割し、ロジックはComposition APIを使って注入するという統一された方針が取りやすくなります。
Composition APIを利用してすべきこと
現時点で明確に実施すべきと思われる施策は、外部ライブラリへの依存をコンポーネントの外に出すことです。
フロントエンド界隈もしくはJavaScript界隈は流行り廃りのサイクルが非常に高速なので、 外部ライブラリへの依存はComposition APIを使ってコンポーネントから見えない(≒importしない)ようにしておくとよいでしょう。
つまり、Vue 3.0以降のコンポーネントにおいてscript部の先頭で記述されるimport文は次の4種類のいずれかになります。
- vue(もしくはNuxt.jsのようなメインで利用するフレームワーク)からのimport
- vueの公式ライブラリからのimport
- 開発者が作成したCompositionからのimport
- 開発者が作成したコンポーネントのimport
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useDateTimeFormatter } from '@/compositions/datetime'
import ChildComponent from '@/components/ChildComponent.vue'
//......
</script>
最近は日付の処理にdayjsをよく使います。 ちょっと前まではmoment.jsが選ばれることが多かったように思います。
以下のように依存をCompositionで隠蔽しておけば、ライブラリを変えたくなったときも修正が簡単です。
import dayjs from 'dayjs'
export const useDateTimeFormatter = (template: string) => {
const formatter = (date: Date) => dayjs(date).format(template)
return { formatter }
}
一般に「Compositionはコンポーネントに対してデータと手続きを提供するが、どのようにデータを取得・処理しているかは隠蔽する」ように責務の境界を設けるときれいにまとまると思います。 Compositionは適切なタイミングで適切な形式のデータを提供することを責務とします。 コンポーネントは必要になったときにデータが利用できるようになってさえいれば良いと考え、 CompositionがAPI経由でデータを取るのか、ブラウザのストレージからデータを取るのか、Workerを経由してなんかするのか、といった話には一切関与しません。 このへんの思想はリポジトリパターンとかに近いかもしれません。
Composition APIに過度に期待してはいけないこと
ロジックの再利用をComposition APIのメリットとして掲げる記事は鵜呑みにしないほうがいいかもしれません。 あるロジックを再利用すべきかどうかは、Composition APIの利用可否とは関係なく検討すべき事項だからです。
確かに、Vue 2.xの記法の制約によりロジック部を切り出して別ファイルにしてもコードが汚くなってしまっていたという問題があったことも、 Composition APIによってデータとそれに対する処理がコンポーネントから分離可能になったことでロジックの再利用が実現しやすくなることも事実です。 先述の通り、あくまでコンポーネントが関心を持つべきでない処理をコンポーネントから分離できることが重要であり、分離した結果それが再利用できるかどうかはまったく別の問題です。 言うまでもありませんが、再利用が可能かどうかを基準としてCompositionを切り出すかどうかを判断するのは愚策です。
- 多くの場合、styleを変えるために処理中の状態をテンプレートに渡すことになるので↩