Vue.js Advent Calendar 2018 13日目の記事です。
この記事は、社内ナレッジベース向けに書いた記事を Qiita 向けに再編集したものです。
2018/10/25,16 に開催された React Conf. 内で発表された React Hooks 、各方面に悲喜交交大わらわで大変盛り上がっててフロントエンド界隈の注目を一手に集めている感じですね。
今まで Stateful なコンポーネントを書くには Class Component を使わなければならなかった React が、 Class を放棄するために State を Functional Component でも扱えるようにしたもの、というようにあちこちで紹介されています。
そんな中、Vue.js を作った Evan You が React Hooks を Vue に導入したらどうなるか、みたいなサンプルコードを Twitter でぶっこんできました。
(PoC 用のリポジトリ作ったり、2018/11/3 の Vue Fes Tokyo では Vue 3.0 にで追加される機能の一つに Hooks API が挙げられるなど、かなり前向きに検討中のようです)
これだけ見ると全然良さが伝わらなくって、
new Vue({
data: {...},
computed: {...}
})
でええのでは?と思ってましたが、Evan 自身の補足(Vue の今の API を代替すると言うよりは、Mixin を代替するというのが近い) を読んで「あーそういうことね完全に理解した」となったので説明します。
対象読者
- React Hooks API を Functional Component 内で State とかが使えるようになる API と理解していて Vue で Hooks やる意味がいまいちわかってない人
- Vue 3.0 で Hooks API が来るらしいので使い所をざっくり知りたい人
Vue Mixins API
そもそも論として、Vue Mixins API とはなんなのか、そしてその問題点とは何か、について整理しておきます。
ミックスイン — Vue.js に概要が書かれています。端的にまとめると、
- 複数のコンポーネントでの共通した振る舞いを抽出するために利用される
- ミックスイン自体は Vue コンポーネントオプションに マージ されるオブジェクト、もしくは Vue コンポーネント
- ライフサイクルメソッドはミックスイン側のものが先に呼ばれ、コンポーネント側のものが後に呼ばれるようになる(同じフックで登録しても全て呼ばれる)
- その他のオプションはミックスイン側とコンポーネント側でキーのコンフリクトがあった場合、コンポーネント側のオプションが優先される。
便利ですね。しかし同様の機能を持っていた React は過去採用していた Mixins を廃止しています。
- 暗黙の依存を生み出す
- ミックスインがコンポーネント側の State などを利用していた場合、キーの名前を変えたいと思ってもエディタの一斉置換で置換されない
- ミックスインが別のミックスインに依存していることも往々にしてあるのでどこを変えてどこを変えてないんだか...
- 名前の衝突
- ミックスインは元のコンポーネントにマージされるので、メソッドや State の名前空間が衝突する危険性がある
- Vue では公式のスタイルガイドで推奨しているように、
$_SomeAwesomeMixin_
など$_ + ミックスイン名
のプリフィクスをミックスインのプロパティ名につけるのが慣習
- Vue では公式のスタイルガイドで推奨しているように、
- ミックスインは元のコンポーネントにマージされるので、メソッドや State の名前空間が衝突する危険性がある
- 複雑さが雪だるま式に増加する
- 最初はシンプルなものだったとしても、ミックスイン同士が互いに密結合になりやすいのであっという間に複雑さが増す
などなどといった理由から React は Mixins を廃止したようです。
当然同様の機能を持つ Vue Mixins も同様の欠陥を抱えています。
加えて、Vue.js 2.5 で TypeScript 対応していますが、Mixins で取り込んだプロパティは型推論に反映されづらい、という落とし穴もあります。
Vue Hooks API?
本題に戻りましょう。
Evan 先生は React Hooks にインスパイアされて下のようなサンプルコードを Twitter に載せました。
import { hooks, useData, useComputed } from 'vue-hooks'
Vue.use(hooks)
new Vue({
template: `
<div @click="data.count++">
{{ data.count }} {{ double }}
</div>
`,
hooks() {
const data = useData({
count: 0
})
const double = useComputed(() => data.count * 2)
return {
data,
double
}
}
})
少しづつ見ていきましょうか。
import { hooks, useData, useComputed } from 'vue-hooks'
おなじみの import 文ですね。
- hooks API サポートを Vue に組み込むためのプラグイン本体
- リアクティブな双方向バインディングをサポートするローカルステートを作るための
useData
- 依存パラメータが更新されるまではキャッシュされたデータを返す算出プロパティを作るための
useComputed
を import してきてます。
プラグインの組み込み宣言(Vue.use(hooks)
)は説明を割愛します。
Vue のコンストラクタ呼び出しのオプション内部を見てみましょうか。
まず hooks() { ... }
の中身から。
hooks() {
const data = useData({
count: 0
})
const double = useComputed(() => data.count * 2)
return {
data,
double
}
}
この関数自体は、カスタムフック含めて各種 Vue Hooks をセットアップし、ViewModel のコンテキストにマージするためのオブジェクトを返却するものだと推測されます。
なので、テンプレートから以下のプロパティが利用可能になっています。
- data:
{ count: number }
- double:
number
-
data.count
を2倍した値を返す算出プロパティ
-
template: `
<div @click="data.count++">
{{ data.count }} {{ double }}
</div>
`,
先程セットアップした Vue Hooks をテンプレートから利用する形で DOM を構築しています。
この div
要素をクリックすると data.count
がインクリメントされ、それに依存する double
の値も変化する、という内容です。
ここまで見ると、確かに普通にコンポーネントオプションを書くのと同じな気がしますね。
Hooks API が Mixins API を置き換える理由
では、どうやって Hooks API が Mixins API を駆逐するのでしょうね。
その答えは、この記事 (React Hooksの概要#Custom Hooks) にありました。
カスタムフックです。
フックを使う関数を新しいカスタムフックとして、コンポーネント側から利用できる、というものです。
例えば、こんな感じになります。
import { hooks, useData, useComputed } from 'vue-hooks'
Vue.use(hooks)
// カスタムフック
const useCount = () => {
const count = useData(0)
const double = useComputed(() => count * 2)
return {
count,
double
}
}
new Vue({
template: `
<div @click="countHooks.count++">
{{ countHooks.count }} {{ countHooks.double }}
</div>
`,
hooks() {
const countHooks = useCount()
return {
countHooks
}
}
})
先程のアプリケーションを、カスタムフックを使って置き換えたものです。
一見するとミックスインと同じように共通処理を定義して掃き出しているように見えますが、ミックスインと比べると以下の利点があります。
- 名前が衝突しない
- コンテキストに反映させるときの名前は、利用するコンポーネント側で自由に設定できるので名前が衝突しない
- コンポーネントのスコープを汚さない
- コンポーネントのスコープとフックのスコープが異なるので、影響がない
- 他のフックともスコープが別になるので影響がない
- コンポーネントから渡るプロパティとフックから渡されるプロパティが明確
- 関数の引数や返り値としてやり取りするので、ステートのキーを変えたい場合でも困らない
- 逆にフック側のみで使いたいプロパティを秘匿することも出来る、カプセル化の実現
- TypeScript を使っている場合、型推論を利用できる
今まで Mixins を使ってきたときの悩みどころをきれいに解決している...!!
欠点としては利用の際のコード記述量が増えるくらいですかね。
一応同じフックに依存する複数のフック間で、依存先のフックの状態が共有されないのはあるんですが、冷静になってみるとそれはダイヤモンド型の継承なので、元からアンチパターンですよね。
まとめ
というわけで、基本的に Vue Mixins は Vue 3.0 以降 Hooks API で置き換えられるものになりそうだぞ、という話をしてきました。
Evan 先生の載せてたサンプルコードが useData
と useComputed
の二種類しか使ってなかったのであえて触れませんでしたが、React Hooks API には他にも副作用を扱う useEffects
など便利に使えそうなビルトインフックが幾つか用意される予定なので、Vue が Hooks API を採用した場合にもこの類のフックを利用できるようになるんじゃないかなーという気がしています。