【Vue.js】2系から3系へのステップアップ

[Increments × cyma (Ateam Inc.) Advent Calendar 2020](https://qiita.com/advent-calendar/2020/increments-cyma) の10日目は、株式会社エイチーム EC事業本部の @nagimaruxxx が担当します。


# はじめに
私は2020年3月にcymaにフロントエンドエンジニアとして中途入社しました。
入社後はデザイナーと連携を取りながら施策を行ったりシステムの改善業務を行っています。

フロントエンドエンジニアとして入社したものの、訳あって今はほとんどRailsを書いています。そこで気づいたのです。

私「(...フロントエンドの技術が全然アップデート出来ていないな？)」

最近は触り始めて3ヶ月のRailsの勉強ばかりしていて、Vue3への脳内アップデートが出来ていなかったのでこれを機に触ってみました。

# 同じ仕様を満たすコードでどれくらい違うのか
サンプルとして名前一覧を表示し、名前の追加と削除が出来るコードを書きました。こんなイメージ。

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="js,result" data-user="nagimaruxxx" data-slug-hash="abmOxNj" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue-sample">
  <span>See the Pen <a href="https://codepen.io/nagimaruxxx/pen/abmOxNj">
  Vue-sample</a> by nagimaruxxx (<a href="https://codepen.io/nagimaruxxx">@nagimaruxxx</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 開発環境
2系、3系どちらもVue-CLIで構築されています。

## Vue2系での書き方
```html
<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です。

```html
<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](https://v3.vuejs.org/guide/component-provide-inject.html#provide-inject)を使って実装してみます。

### provide / inject って？
> 通常、親コンポーネントから子コンポーネントにデータを渡す必要がある場合は、`props`を使用します。
> 深くネストされたコンポーネントがあるとします。子コンポーネントは親コンポーネントからある値を必要としている場合、`provide/inject`を使用できます。
> コンポーネントのネストがどれほど深いかにかかわらず、それらが同じ親チェーン内にある限り、祖先コンポーネントが、自身の子孫コンポーネント全てに対する依存オブジェクトの注入役を務めることができるようにするために利用されます。

※一部[公式ドキュメント](https://v3.vuejs.org/guide/component-provide-inject.html)から引用

つまり親、子、孫の関係にあるComponentがあるとして、親→孫に値を渡したい時はpropsを使って子を経由する必要があったけど、これを使えば子を経由せずに値を渡せるよ〜的な機能です。

上記のコードを以下の複数コンポーネントに分割し、状態管理してみます。

- NameList (親コンポーネント)
    - NameCounter (表示された人数の合計)
    - NameListView (名前一覧)
    - NameAddArea (名前を追加するInputとボタン部分)

まずはロジック部分を抜き出します。全体像としてはこうです。

```js:js/NameListStore.js
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()`で定義された変数を提供するようになります。

```vue:NameList.vue
<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`を利用することでコンポーネント間の状態が保持されるようになります。

```vue:components/NameCounter.vue
<template>
  <p>人数: {{ list.items.length }}</p>
</template>

<script>
import { injectStore } from "@/js/NameListStore.js"

export default({
  setup() {
    const { list } = injectStore();
    return {
      list
    };
  }
})
</script>
```

```vue:components/NameListView.vue
<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>
```

```vue:components/NameAddArea.vue
<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](https://qiita.com/advent-calendar/2020/increments-cyma) の11日目は、Increments株式会社の(@zumi0)さんがお送りします！