【Vue.js】Composition API時代の便利ライブラリ「VueUse」を使ってみた

[Vue Composition API](https://composition-api.vuejs.org/) によって Vue.js にも React Hooks のようなロジックの再利用性の高い開発体験がもたらされようとしています。
しかし、まだ「Composition API の良さをわかっていない」という方や「Composition API をうまく利用した書き方がわからない」という方も多いかと思います。

本記事では Composition API 時代の便利ライブラリ **VueUse** を用いた実装例や、 VueUse 自体の実装がどのようなものか紹介します。
Composition API の良さや雰囲気もキャッチアップしていただければ幸いです。

# VueUse とは？
[VueUse](https://vueuse.js.org/) は [Anthony Fu](https://twitter.com/antfu7) さん[^1]が中心に開発しているライブラリで、Composition API を用いた便利系関数を数多く集めたライブラリです。

例えば、ブラウザ上のマウスポインタの座標をリアクティブに取得する `useMouse()`, ブラウザ API の [localStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage) を使って状態を保持できる `useLocalStorage()`, 負荷対策のために連続する関数呼び出しを防ぐ `useDebounceFn()` [^2]などといった関数が提供されています。

![vueuse.js.org.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/a6d099b6-b8cb-2190-87a8-744ce3bf6107.png)

公式サイト: [https://vueuse.org/](https://vueuse.org/)
GitHub: [vueuse/vueuse](https://github.com/vueuse/vueuse)
npm: [@vueuse/core](https://www.npmjs.com/package/@vueuse/core)

# 検証環境の構築
GitHub リポジトリの Description に記載通り、Vue 2系・3系のどちらでも利用可能です:

> 🧰 Collection of Composition API utils for Vue 2 and 3 https://vueuse.js.org/

今回は Vue CLI で立ち上げた Vue 2.6 系のプロジェクトに [@vue/composition-api](https://www.npmjs.com/package/@vue/composition-api) をプラグインとして追加した環境で検証します。

## 動作確認した環境
- macOS Catalina
- Chrome 83
- Node.js 12.18.1
- npm 6.14.5
- Vue CLI v4.4.4
    - `vue` 2.6.11
- `@vue/composition-api` 0.6.5
- `@vueuse/core` 2.0.34

## Vue CLI のインストール
```
npm i -g @vue/cli
```

Vue CLI をグローバルインストールしたくない方は、以下の手順の `vue` コマンド部分を `npx @vue/cli` に読み替えていただいても大丈夫です。

## プロジェクトの作成
`vue-2-vueuse-trial` というプロジェクト名で環境を構築していきます:

```
vue create vue-2-vueuse-trial
```

設定は以下のようにしました:

```
Vue CLI v4.4.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Pick a linter / formatter config: Basic
? 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
```

## プラグインと VueUse の導入
プロジェクトディレクトリが出来上がったら、[@vue/composition-api](https://www.npmjs.com/package/@vue/composition-api) と VueUse を `npm install` します:

:::note warn
(2023/02/25 追記) Vue 2.7, Vue 3.x の場合は `@vue/composition-api` のインストールは不要です
:::

```
cd vue-2-vueuse-trial
npm i @vueuse/core @vue/composition-api
```

`src/main.ts` に `Vue.use(VueCompositionAPI)` を追加します[^3]:

```diff:src/main.ts
 import Vue from 'vue'
+import VueCompositionAPI from '@vue/composition-api'
 import App from './App.vue'


+Vue.use(VueCompositionAPI)
 Vue.config.productionTip = false
```

`npm run serve` で開発ビルドを開始してください。

# VueUse の関数を使ってみる
## `useMouse()`
リアクティブにマウスポインタの座標が取得できる `useMouse()` を使ってみます:

```vue:src/App.vue
<template>
  <div>
    {{ x }}, {{ y }}
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api'
import { useMouse } from '@vueuse/core'

export default defineComponent({
  setup() {
    // tracks mouse position
    const { x, y } = useMouse()

    return { x, y }
  }
})
</script>
```

![useMouse.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/6b34edee-8ca7-24ca-f93d-c2176926dc14.gif)
これだけでマウスポインタの座標がリアクティブに反映されていることがわかるかと思います。

`useMouse()` の実装を確認すると `useEventListener()` という関数を使っていて、 `useEventListener()` はコンポーネントのマウント時(Composition API の `onMounted()` を利用)にイベントリスナーを追加していることがわかります:
https://github.com/antfu/vueuse/blob/master/packages/core/useMouse/index.ts
https://github.com/antfu/vueuse/blob/master/packages/core/useEventListener/index.ts

VueUse ではこのような**関数がより抽象的な関数を参照している**パターンの実装が所々に見られます。
Composition API を用いた良い実装の例として知っておくと良いかと思います。

## `useLocalStorage()`
ブラウザ API の [localStorage](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage) を使って状態を保持できる `useLocalStorage()` を使ってみます。

まずは `useLocalStorage()` を使わず、 VueUse がなくても利用できる `reactive()` [^4] を使ってみましょう:

```vue:src/App.vue
<template>
  <div>
    <div>
      name: <input v-model="state.name" />
    </div>
    <div>
      color: <input v-model="state.color" />
    </div>
    <div>{{ state }}</div>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from '@vue/composition-api'

export default defineComponent({
  setup() {
    const state = reactive({
      name: 'Apple',
      color: 'red',
    })

    return { state }
  }
})
</script>
```

name, color を変更すると、下に表示されている JSON 形式の `state` の表示も更新される画面が表示されます:

<img width="500" alt="スクリーンショット 2020-06-20 17.33.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/073f09da-3a5c-4481-fba3-2f9c18ba3bbe.png">

上記の実装では name を Banana, color を yellow のように変更してページをリロードすると、元の状態(name が Apple, color が red)に戻ります。

以下のように `<script lang="ts">` ブロックを変更し、`reactive()` の代わりに `useLocalStorage()` を利用するように変更してみます:

```typescript
import { defineComponent } from '@vue/composition-api'
import { useLocalStorage } from '@vueuse/core'

export default defineComponent({
  setup() {
    // persist state in localStorage
    const state = useLocalStorage(
      'my-storage',
      {
        name: 'Apple',
        color: 'red',
      },
    )

    return { state }
  }
})
```

![useLocalStorage.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/465e196c-8f6d-d962-ad2d-e9a722ec684e.gif)

`state` が localStorage に保存されるようになったので、ページをリロードしても状態が保持されるようになりました。

サンプルアプリとしてよくある ToDo リストの状態管理に `useLocalStorage()` を使うようにすると、手軽にデータを保存できる ToDo リストにできて楽しいかもしれません。

# 公式サイトが Storybook でできている件
:::note warn
(2023/02/25 追記) 現在は [Vitepress](https://vitepress.vuejs.org/) が利用されています
https://github.com/vueuse/vueuse/pull/277
:::

[公式サイト](https://vueuse.js.org/)が Storybook でできていて、各関数を即座に試せるリファレンスとなっています。

![vueuse.js.org__path=_story_sensors--usemouse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/82ba3785-4391-a2c4-4b0b-af52f6229fa6.png)

各関数のページ下部には関数の "Source" へのリンクがあり、ソースを見てどのような実装になっているか追っていくと Composition API を用いた良い実装の勉強となるかと思います。

# 所感
VueUse は多くの便利関数を提供しているので、今後お世話になる可能性が高いライブラリだと思いました。
まだ試せていない関数が多くあるので、使ってみたりコードを読んだりしてみようかと思います。

[^1]: [@vue/composition-api](https://www.npmjs.com/package/@vue/composition-api) (Vue 2系用プラグイン)のメンテナーでもあります
[^2]: `useDebounceFn()` は Composition API に依存しない純粋な JS の関数となっています。似た名前の関数である `useDebounce()` は Composition API と `useDebounceFn()` に依存していることに気をつけてコードを読んでみると良いかと思います
[^3]: VueUse の名称は関数名のプレフィックスとして `use` を付ける慣習に由来する物であり、`Vue.use()` との関連性はないと思います。なお、Vue 3系からは `Vue.use()` は App インスタンスに対するオプションに変更される予定(rfc: [0009-global-api-change.md](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md))のため、発話上 VueUse と `Vue.use()` を区別できない問題は将来的には解消されると思います
[^4]: Vue 2系では `import { reactive } from '@vue/composition-api'` による `reactive` は `Vue.observable` 相当となります。 Vue 3系では `import { reactive } from 'vue'` のようにして import します