Increments × cyma (Ateam Inc.) Advent Calendar 2020 の10日目は、株式会社エイチーム EC事業本部の @nagimaruxxx が担当します。
はじめに
私は2020年3月にcymaにフロントエンドエンジニアとして中途入社しました。
入社後はデザイナーと連携を取りながら施策を行ったりシステムの改善業務を行っています。
フロントエンドエンジニアとして入社したものの、訳あって今はほとんどRailsを書いています。そこで気づいたのです。
私「(...フロントエンドの技術が全然アップデート出来ていないな?)」
最近は触り始めて3ヶ月のRailsの勉強ばかりしていて、Vue3への脳内アップデートが出来ていなかったのでこれを機に触ってみました。
同じ仕様を満たすコードでどれくらい違うのか
サンプルとして名前一覧を表示し、名前の追加と削除が出来るコードを書きました。こんなイメージ。
See the Pen Vue-sample by nagimaruxxx (@nagimaruxxx) on CodePen.
開発環境
2系、3系どちらもVue-CLIで構築されています。
Vue2系での書き方
<div id="app">
<p>人数: {{ list.length }}</p>
<ul>
<li v-for="(item, i) in list" :key="item">{{ item }}
<button @click="removeItem(i)">x</button>
</li>
</ul>
<input type="text" v-model="inputItem">
<button @click="addItem">add</button>
</div>
<script>
new Vue({
el: "#app",
data: {
list: ["山田", "佐藤", "高橋"],
inputItem: "",
},
methods: {
addItem: function() {
this.list.push(this.inputItem)
},
removeItem: function(index) {
this.list.splice(index, 1)
}
}
})
</script>
このままVue3系にアップデートしてエラーを消す作業をしてみます。
Vue3系での書き方
※バージョンは3.0.3です。
<template>
<div>
<p>人数: {{ list.items.length }}</p>
<ul>
<li v-for="(item, i) in list.items" :key="item">{{ item }}
<button @click="removeItem(i)">x</button>
</li>
</ul>
<input type="text" v-model="inputItem">
<button @click="addItem">add</button>
</div>
</template>
<script>
import { ref, reactive } from "vue";
export default({
setup() {
const list = reactive({
items: ["山田", "高橋", "佐藤"]
});
const inputItem = ref("");
const addItem = () => {
list.items.push(inputItem.value);
inputItem.value = "";
};
const removeItem = (index) => {
list.items.splice(index, 1);
};
return {
list,
inputItem,
addItem,
removeItem
};
}
})
</script>
書き直してみました。
v-for
などのディレクティブには変化がありませんが、<script>
の中身がだいぶ変わったように思います。
Vue2系でdata()
に相当するリアクティブなプロパティはsetup()
内でreactive
またはref
で宣言します。オブジェクトを宣言する時はreactive
、それ以外はref
を使います。
またtemplate内で使う変数はsetup()
内でreturnする必要があります。ロジックも2系ではmethods, computed
に記述する必要がありましたが、setup()
内で宣言出来るようになっています。
3.1系では<component>
でSyntax Sugarを使えるようになっていたり、<script setup>
とすることでexport default
の記述が不要になったり記述が簡素になる予定です。
Composition APIを使ってみる
さて、Vue3で新しく追加された機能で一番期待されているのはComposition APIではないでしょうか。
早速Composition APIの Provide / injectを使って実装してみます。
provide / inject って?
通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合は、
props
を使用します。
深くネストされたコンポーネントがあるとします。子コンポーネントは親コンポーネントからある値を必要としている場合、provide/inject
を使用できます。
コンポーネントのネストがどれほど深いかにかかわらず、それらが同じ親チェーン内にある限り、祖先コンポーネントが、自身の子孫コンポーネント全てに対する依存オブジェクトの注入役を務めることができるようにするために利用されます。
※一部公式ドキュメントから引用
つまり親、子、孫の関係にあるComponentがあるとして、親→孫に値を渡したい時はpropsを使って子を経由する必要があったけど、これを使えば子を経由せずに値を渡せるよ〜的な機能です。
上記のコードを以下の複数コンポーネントに分割し、状態管理してみます。
- NameList (親コンポーネント)
- NameCounter (表示された人数の合計)
- NameListView (名前一覧)
- NameAddArea (名前を追加するInputとボタン部分)
まずはロジック部分を抜き出します。全体像としてはこうです。
import { ref, reactive, inject, provide } from "vue"
export const createStore = () => {
const inputItem = ref("");
const list = reactive({
items: ["山田", "高橋", "佐藤"]
});
const addItem = () => {
list.items.push(inputItem.value);
inputItem.value = "";
};
const removeItem = (index) => {
list.items.splice(index, 1);
};
return {
list,
inputItem,
addItem,
removeItem
}
}
// キーを作成
export const key = Symbol()
export const provideStore = () => {
provide(key, createStore());
}
export const injectStore = () => {
return inject(key);
}
ただファイルを分割するだけではコンポーネント間で状態が保持されません。
依存性の注入(DI)が必要なため、Symbol()を利用してストアごとに一意のキーを用意します。
キーの作成とロジック部分の抜き出しが出来たら、親コンポーネントを作成します。
親コンポーネントは子孫コンポーネントにprovideStore()
の中身、つまりNameListStore.js/createStore()
で定義された変数を提供するようになります。
<template>
<div>
<NameCounter />
<NameListView />
<NameAddArea />
</div>
</template>
<script>
import NameCounter from "./components/NameCounter.vue"
import NameListView from "./components/NameListView.vue"
import NameAddArea from "./components/NameAddArea.vue"
import { provideStore } from "@/js/NameListStore.js"
export default({
components: {
NameCounter,
NameListView,
NameAddArea
},
setup() {
provideStore()
}
})
</script>
続けて子コンポーネントを作成します。親から提供されたKey
を利用することでコンポーネント間の状態が保持されるようになります。
<template>
<p>人数: {{ list.items.length }}</p>
</template>
<script>
import { injectStore } from "@/js/NameListStore.js"
export default({
setup() {
const { list } = injectStore();
return {
list
};
}
})
</script>
<template>
<ul>
<li v-for="(item, i) in list.items" :key="item">{{ item }}
<button @click="removeItem(i)">x</button>
</li>
</ul>
</template>
<script>
import { injectStore } from "@/js/NameListStore.js"
export default({
setup() {
const { list, removeItem } = injectStore();
return {
list,
removeItem
};
}
})
</script>
<template>
<div>
<input type="text" v-model="inputItem">
<button @click="addItem">add</button>
</div>
</template>
<script>
import { injectStore } from "@/js/NameListStore.js";
export default({
setup() {
const { inputItem, addItem } = injectStore();
return {
inputItem,
addItem
}
}
})
</script>
これで、provide/injectを使った実装が出来ました!🙌
参考にした記事
https://qiita.com/karamage/items/4bc90f637487d3fcecf0
https://qiita.com/ryo2132/items/f055679e9974dbc3f977
https://qiita.com/mgr/items/a5e35636d371969e0a4d
https://ushirock.hateblo.jp/entry/2020/07/12/220235
https://speakerdeck.com/kazupon/matinimatuta-vue-dot-js-3
まとめ
Vue2からVue3からどう変わったのか、そしてComposition APIを使ってみた記事でした。久々にVue.jsを触ってみてバージョン上がるたびに難しくなるな〜とは感じていましたが、一つずつ追っていけば意外と怖くなかったです。
また今回の記事てはVue3で公式サポートしたTypeScriptを導入していませんが、Vue-CLIを利用してTypeScriptが利用できる環境を簡単に用意することができるのでより堅牢な実装がしたい時はTypeScriptを導入しましょう。
この記事をみて、バージョンアップされたVue.jsを触る機会になってくれれば幸いです。
Increments × cyma (Ateam Inc.) Advent Calendar 2020 の11日目は、Increments株式会社の(@zumi0)さんがお送りします!