Vue 3 Composition API を使ってみよう

# Composition APIとは、

Composition APIとは、2020年09月18に正式リリースされた、[Vue 3](https://v3.vuejs.org/)に追加された目玉機能です。

Composition APIはコンポーネントを構築するための新しい手法です。
Vueが抱えていた以下の課題を解決するためのものです。

- TypeScriptのサポート
- ロジックの再利用の難しさ
- アプリケーションが巨大になると、コードの把握が難しくなる

# Vue CLIでVue 3を導入する

早速、Vue CLIでVue 3を使っていきましょう。
まずは、最新のVue CLIをインストールします。

```sh
yarn global add @vue/cli@next
# OR
npm install -g @vue/cli@next
```

```sh
vue -V
@vue/cli 4.5.4
```

次に、いつも通りプロジェクトを作成します。
Vue 3が選べるようになっています。


```sh
vue create vue3-project

Vue CLI v4.5.4

? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 
```

今回は、Manually select featuresから選択します。

```sh
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
```

プロジェクトの作成が完了したら、早速起動してみましょう。

```sh
cd vue3-project
npm run serve
```

![スクリーンショット 20201017 20.19.14.png](https://firebasestorage.googleapis.com/v0/b/app-blog-1ef41.appspot.com/o/articles%2FmDWbGa03Jy9xYqMC1tn8%2F130e6d1f5c716ec5f87f3686ba5f4add.png?alt=media&token=cb03966c-5165-464d-b38d-56cf0e959396)


TODOアプリの作成を例にして、Composition APIを使っていきましょう。

# コンポーネントの例

まずは、Composition APIで作成されたコンポーネントの全体像を見ていきましょう。
次のように、以前の元とは大きく変わっていることがわかります。

```html
// MyTodo.vue
<template>
  <todo-list
    v-for="todo in sortTodo"
    :todo="todo"
    :key="todo.id"
    @toggle="toggleTodo"
    @remove="removeTodo"
  />
  <add-todo
    @add="addTodo"
  />
</template>

<script lang="ts">
import { computed, defineComponent, reactive, watchEffect, onMounted } from 'vue'
import TodoList from '@/components/TodoList.vue'
import AddTodo from '@/components/AddTodo.vue'
import { fetchTodo } from '@/api'
import { Todo } from '@/types/todo'
import { v4 as uuid } from 'uuid'

interface State {
  todos: Todo[];
}

export default defineComponent({
  components: {
    TodoList,
    AddTodo
  },
  setup () {
    const state = reactive<State>({
      todos: []
    })

    onMounted(async () => {
      state.todos = await fetchTodo()
    })

    const sortTodo = computed(() => state.todos.sort((a, b) => {
      return b.createdAt.getTime() - a.createdAt.getTime()
    }))

    const addTodo = (title: string) => {
      state.todos = [...state.todos, {
        id: uuid(),
        title,
        done: false,
        createdAt: new Date()
      }]
    }

    const removeTodo = (id: string) => {
      state.todos = state.todos.filter(todo => todo.id !== id)
    }

    const toggleTodo = (id: string) => {
      const todo = state.todos.find(todo => todo.id === id)
      if (!todo) return
      todo.done = !todo.done
    }

    watchEffect(() => console.log(state.todos))

    return {
      sortTodo,
      addTodo,
      removeTodo,
      toggleTodo
    }
  }
})
</script>
```

```html
// TodoList.vue
<template>
  <div>
    <span>{{ todo.title }}</span>
    <input type="checkbox" value="todo.done" @change="toggle" />
  </div>
  <div>
    {{ date }}
  </div>
  <div>
    <button @click="remove">削除</button>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { Todo } from '@/types/todo'

export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>
    }
  },
  emits: ['toggle', 'remove'],
  setup (props, context) {
    const date = computed(() => {
      if (!props.todo) return
      const { createdAt } = props.todo
      return `${createdAt.getFullYear()}/${createdAt.getMonth() + 1}/${createdAt.getDate()}`
    })

    const toggle = () => {
      context.emit('toggle', props.todo!.id)
    }

    const remove = () => {
      context.emit('remove', props.todo!.id)
    }

    return {
      date,
      toggle,
      remove
    }
  }
})
</script>
```

```html
// addTodo
<template>
  <input type="text" v-model="state.inputValue" />
  <button @click="onClick" :disabled="state.hasError">追加</button>
  <p v-if="state.hasError" class="error">タイトルが長すぎ！</p>
</template>

<script lang="ts">
import { defineComponent, reactive, watchEffect } from 'vue'

interface State {
  inputValue: string;
  hasError: boolean;
}
export default defineComponent({
  emits: ['add'],
  setup (_, context) {
    const state = reactive<State>({
      inputValue: '',
      hasError: false
    })

    const onClick = () => {
      context.emit('add', state.inputValue)
      state.inputValue = ''
    }

    watchEffect(() => {
      if (state.inputValue.length > 10) {
        state.hasError = true
      } else {
        state.hasError = false
      }
    })

    return {
      state,
      onClick
    }
  }
})
</script>

<style scoped>
.error {
  color: red;
}
</style>
```

以前のような構造（これをOptions APIと呼びます）と異なり、data・methods・computed・ライフサイクルメソッドなどの区別がなくなり、全てが**setup**メソッドの中に記述されています。一方で、componentsやpropsの記述は以前と変わりありません。

全てがsetupメソッド内に記述された結果、他のプロパティにアクセスする際に`this`が不要になりました。以前までのVueではこの`this`の制約によりアロー関数で記述することが敬遠されていたのですが、Composition APIではアロー関数により記述が可能になりました。

setupメソッド内のデータは、returnされたものだけがtemplate内で使用できるようになります。
そのため、例えば`MyTodo.vue`のsetupメソッドでは`state`が宣言されていますがreturnされていないためこれを使用することはできません。`state`の値は直接使用せずに、`computed`を通して使用するという意図を伝えることができます。

それでは、もう少し具体的にここのプロパティを見ていきましょう。

# コンポーネントの宣言

Vue 3では、今までの`Vue.extend`の宣言に変わり、`defineComponent`が使われます。これにより型推論が効くようになります。JavaScriptで記述する場合には必要ありません。

```html
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({})
</script>
```

# リアクティブなデータ　reactive または ref

`reactive`または`ref`は以前の`data`に相当するものです。どちらもジェネリクスで型定義を渡すことができます。

## reactive

個人的に、`reactive`のほうが以前のdataに近い印象を受けます。reactiveは1つのオブジェクトとしてデータを定義します。

```ts
interface State {
  inputValue: string;
  hasError: boolean;
}
const state = reactive<State>({
  inputValue: '',
  hasError: false
})
```

`reactive`の値にはオブジェクト形式でアクセスします。

```ts
state.inputValue
state.hasError
```

### 分割代入すると、リアクティブにならない

`reactive`を使用する注意点として、分割代入した値に対してはリアクティブ性が失われるという点があります。

`AddTodo`コンポーネントを例に試してみましょう。

```html
<template>
  <input type="text" v-model="inputValue" />
  <button @click="onClick" :disabled="hasError">追加</button>
  <p v-if="hasError" class="error">タイトルが長すぎ！</p>
</template>

<script lang="ts">
import { defineComponent, reactive, watchEffect } from 'vue'

interface State {
  inputValue: string;
  hasError: boolean;
}
export default defineComponent({
  emits: ['add'],
  setup (_, context) {
    // 分割代入でここのプロパティを受け取るように変更
    let { inputValue, hasError } = reactive<State>({
      inputValue: '',
      hasError: false
    })

    const onClick = () => {
      context.emit('add', inputValue)
      inputValue = ''
    }

    watchEffect(() => {
      if (inputValue.length > 10) {
        hasError = true
      } else {
        hasError = false
      }
    })

    return {
      inputValue,
      hasError,
      onClick
    }
  }
})
</script>

<style scoped>
.error {
  color: red;
}
</style>
```

![reactive.gif](https://firebasestorage.googleapis.com/v0/b/app-blog-1ef41.appspot.com/o/articles%2FmDWbGa03Jy9xYqMC1tn8%2F49c27133b588385cbe5fcdf65545d7d5.gif?alt=media&token=6e2b4b98-2d64-4ef4-a2cc-1fd63b06d3bd)

リアクティブ性が失われていることがわかります。

このような状況に対する解決策として、`toRefs`関数が用意されています。`toRefs`はリアクティブオブジェクトをプレーンオブジェクトに変換します。結果のオブジェクトの各プロパティは、元のオブジェクトの対応するプロパティの参照です。

```html
<template>
  <input type="text" v-model="inputValue" />
  <button @click="onClick" :disabled="hasError">追加</button>
  <p v-if="hasError" class="error">タイトルが長すぎ！</p>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs, watchEffect } from 'vue'

interface State {
  inputValue: string;
  hasError: boolean;
}
export default defineComponent({
  emits: ['add'],
  setup (_, context) {
    const { inputValue, hasError } = toRefs(reactive<State>({
      inputValue: '',
      hasError: false
    }))

    const onClick = () => {
      context.emit('add', inputValue)
      inputValue.value = ''
    }

    watchEffect(() => {
      if (inputValue.value.length > 10) {
        hasError.value = true
      } else {
        hasError.value = false
      }
    })

    return {
      inputValue,
      hasError,
      onClick
    }
  }
})
</script>

<style scoped>
.error {
  color: red;
}
</style>
```

`toRefs`を適用すると、後述する`ref`で宣言されたものと同等の状態になります。そのため、template以外の場所からアクセスする際には、`inputValue.value`のように.valueの値に対してアクセスします。

![reactiveref.gif](https://firebasestorage.googleapis.com/v0/b/app-blog-1ef41.appspot.com/o/articles%2FmDWbGa03Jy9xYqMC1tn8%2F43256c7b804e83e250b574d748281c0f.gif?alt=media&token=8c824889-17c9-4653-a56c-9f27f667f94d)

## ref

リアクティブなデータを宣言するもう一つの方法は`ref`を使用することです。
`ref`はそれぞれの変数として宣言されます。

```ts
let inputValue = ref('')
let hasError = ref(false)
```

`ref`で宣言された値にtemplate以外の場所からアクセスする場合、.valueからアクセスします。

```ts
inputValue.value
hasError.value
```

## reactiveとrefどっちを使えばいい？

このように、Composition APIではリアクティブなデータの宣言に`reactive`と`ref`どちらも使用することができます。しかし、どちらを使用するかのベストプラクティスはまだ存在していないようです。

[Ref vs Reactive](https://composition-api.vuejs.org/#ref-vs-reactive)

Composition APIを効率的に理解するためには、どちらの方法を理解しておくべきだと述べられています。

# メソッド

以前の`methods`に相当するものは、通常のJavaScriptの関数の宣言にになります。

```ts
const addTodo = (title: string) => {
  state.todos = [...state.todos, {
    id: uuid(),
    title,
    done: false,
    createdAt: new Date()
  }]
}

const removeTodo = (id: string) => {
  state.todos = state.todos.filter(todo => todo.id !== id)
}

const toggleTodo = (id: string) => {
  const todo = state.todos.find(todo => todo.id === id)
  if (!todo) return
  todo.done = !todo.done
}
```

メソッドの宣言も同様に、returnしていないものはtemplate内で使用できません。

# computed

computedは関数をcomputedで包むことで実装できます。

```ts
const sortTodo = computed(() => state.todos.sort((a, b) => {
  return b.createdAt.getTime() - a.createdAt.getTime()
}))
```

何度も言うように、computedの値もreturnする必要があります。

# データの監視 watch watchEffect

watchもdataのように`watch`と`watchEffect`2つの方法が使えるようになりました。
以前のような使い方と近いのは`watch`です。

## watch

`watch`は第一に引数に監視する対象のリアクティブな値を指定します。リアクティブな値とは、`ref`、`reactive`、`computed`で宣言された値を指します。

第二引数に実行するメソッドを渡します。

```ts
watch(state.todos, (newTodos, oldTodos) => {
  console.log(oldTodos, newTodos)
})
```

監視対象が複数ある場合、配列で指定します。

```ts
watch([state.inputValue, state.hasError], ([newInputValue, newHasError], [oldInputValue, oldHasError]) => {
})
```

## watchEffect

`watchEffect`は、監視対象を指定しません。computedのように、関数内の値が変更されたときに実行されます。

```ts
watchEffect(() => console.log(state.todos))
```

基本的に、`watchEffect`よりも`watch`のほうが機能が優れているようで、例えば`watchEffect`と比べて`watch`は次のような機能を提供します。

- 副作用の遅延実行。
- 監視対象の値を渡すので、意図が明確。
- 監視状態の以前の値と現在の値の両方にアクセスできる。

# ライフサイクルメソッド

Composition APIのライフサイクルメソッドは`on`というプレフィックスがつけられました。

```ts
onMounted(async () => {
  state.todos = await fetchTodo()
})
```

Options APIとの対応は以下の通りです。

|Options API|Conposition API|
|:------------:|:------------:|
|beforeCreate|use setup()|
|created|use setup()|
|beforeMount|onBeforeMount|
|mounted|onMounted|
|beforeUpdate| onBeforeUpdate|
|updated |onUpdated|
|beforeDestroy | onBeforeUnmount|
|destroyed |onUnmounted|
|activated | onActivated|
|deactivated |onDeactivated|
|errorCaptured | onErrorCaptured|

`beforeCrate`、`crate`に相当するフックはなくなり、setup()で記述するようになりました。
さらに、以下のライフサイクルフックが追加されました。

- onRenderTracked
- onRenderTriggered

# props

propsををsetupメソッド内で使用するために、setupメソッドは引数を受け取ります。第一引数はpropsそのものです。

```ts
export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>
    }
  },
  setup (props, context) {
    const date = computed(() => {
      if (!props.todo) return
      const { createdAt } = props.todo
      return `${createdAt.getFullYear()}/${createdAt.getMonth() + 1}/${createdAt.getDate()}`
    })
    
    return {
        date
    }
})
```

propsプロパティは今まで通りです。
propsプロパティに型定義がされている場合、そのとおりに推論されます。

![スクリーンショット 20201018 14.41.51.png](https://firebasestorage.googleapis.com/v0/b/app-blog-1ef41.appspot.com/o/articles%2FmDWbGa03Jy9xYqMC1tn8%2F3c6191e6359a31b84519bc657986c073.png?alt=media&token=6f1bbc94-7e40-40b4-b4ca-d00dd352ae8b)

注意点として、以下のようにpropsを分割して受け取ると、リアクティブ性が失われてしまいます。

```ts
setup({ todo }, context) {}
```

# emit

setupメソッドの第二引数には、contextオブジェクトを受け取ります。contextオブジェクトからはOptions APIで`this`からアクセスできた一部のプロパティを提供します。

```ts
context.attr
context.slots
context.emit
```

どのプロパティも、先頭の`$`が外れていることに注意してください。
propsは使用せずに、contextオブジェクトだけ使用したいときには、第一引数を`_`にして受け取らないようにします。

```ts
setup(_, context) {}
```

contextオブジェクトの中に`emit`が含まれているので、以前のように使用することができます。

```ts
export default defineComponent({
  emits: ['add'],
  setup (_, context) {
    const state = reactive<State>({
      inputValue: '',
      hasError: false
    })

    const onClick = () => {
      context.emit('add', state.inputValue)
      state.inputValue = ''
    }
    return {
      state,
      onClick
    }
  }
})
```

さらに注目すべき変更点として、Vue 3からは`emits`オブジェクトとしてそのコンポーネントがemitする可能性があるイベントを配列として宣言するようになりました。

必須のオプションではないですが、コードを自己文章化することができ、推論もされるようになります。

![スクリーンショット 20201018 14.53.53.png](https://firebasestorage.googleapis.com/v0/b/app-blog-1ef41.appspot.com/o/articles%2FmDWbGa03Jy9xYqMC1tn8%2F6ae6295cb68a1ae45f0cea322ddd3316.png?alt=media&token=dc05c957-76d7-4fc4-a7e0-6fbc2a9740b1)

# モジュールに分割する

ここまでただ単にComposition APIに書き換えてみただけなので、結局コンポーネント内で宣言されており、また`state`の値に依存しているのであまり恩恵を感じないように思えます。

ここからは、Composition APIの真骨頂であるコードの分割にコマを進めていきましょう。
Composition APIではdata、computed等の区切りがなくなったことで関心ことの分離が可能になりました。

分割した関数は、`src/composables`配下に配置していきます。
それでは、まずは`sortTodo`を切り出してみましょう。

実装は次のようになります。

```ts
// src/composable/use-sort-todo.ts
import { computed, isRef, Ref } from 'vue'
import { Todo } from '@/types/todo'

export default (todos: Ref<Todo[]>) => {
  const sortTodo = computed(() => todos.value.sort((a, b) => {
    return b.createdAt.getTime() - a.createdAt.getTime()
  }))

  return {
    sortTodo
  }
}
```

元々`state.todo`でアクセスしていた箇所を引数で受け取るようにしました。引数の型Ref<T>としています。ロジック部分に変わりはありません。

使用側では以下のように使います。

```ts
import { toRefs, defineComponent, reactive, watchEffect, onMounted } from 'vue'
import useSortTodo from '@/composables/use-sort-todo'

interface State {
  todos: Todo[];
}

export default defineComponent({
  setup () {
    const state = reactive<State>({
      todos: []
    })

    const { todos } = toRefs(state)

    const { sortTodo } = useSortTodo(todos)

    // 中略

    return {
      sortTodo,
      addTodo,
      removeTodo,
      toggleTodo
    }
  }
})
```

これで、sortTodoはVueオブジェクトに依存することなく、再利用可能な関数として取り出すことができました。

その他のメソッドも切り出してみると、だいぶコンポーネントがスッキリしました。

```html
<template>
  <todo-list
    v-for="todo in sortTodo"
    :todo="todo"
    :key="todo.id"
    @toggle="toggleTodo"
    @remove="removeTodo"
  />
  <add-todo
    @add="addTodo"
  />
</template>

<script lang="ts">
import { defineComponent, watchEffect } from 'vue'
import TodoList from '@/components/TodoList.vue'
import AddTodo from '@/components/AddTodo.vue'
import useTodos from '@/composables/use-todos'
import useSortTodo from '@/composables/use-sort-todo'
import useActionTodo from '@/composables/use-action-todo'

export default defineComponent({
  components: {
    TodoList,
    AddTodo
  },
  setup () {
    const { todos } = useTodos()
    const { sortTodo } = useSortTodo(todos)
    const { addTodo, removeTodo, toggleTodo } = useActionTodo(todos)

    watchEffect(() => console.log(todos.value))

    return {
      sortTodo,
      addTodo,
      removeTodo,
      toggleTodo
    }
  }
})
</script>
```

変更はこちらのレポジトリから参照できます。

[https://github.com/azukiazusa1/vue3-project](https://github.com/azukiazusa1/vue3-project)

# 参考

[Vue Composition API](https://composition-api.vuejs.org/#summary)
[Vue3への移行は可能？Vue2のコードをComposition APIで書き換えてみた](https://tech.bita.jp/article/49)
[Vue.js 3.0で搭載される Composition APIをリリースに先駆けて試してみた](https://tech.smartcamp.co.jp/entry/try-vue-composition-api)
[Vue 3 に向けて Composition API を導入した話](https://developers.microad.co.jp/entry/2020/08/11/063000)
[Vue 2.xのOptions APIからVue 3.0のComposition APIへの移行で知っておくと便利なTips](https://techblog.zozo.com/entry/vue-options-api-to-composition-api)
[Vue3リリース直前！導入されるcomposition APIを使ってみよう](https://toranoana-lab.hatenablog.com/entry/2020/01/28/181251)