【Vue3.3】Composableの基本からTipsまで

## はじめに

Vue3 よりコンポーサブルという新しい概念が生まれました
これは処理のコンポーネント化と同義で、ロジックを切り離して使いまわそう！という考え方です

概念は理解できた、サンプルコードも読めた！
ただ実践で使うと、値が変わらない！呼び出し方が分からない！と少々難しい

てことで整理しました

※ 各種 import は省略しています（自動インポートを設定済み）

Vue 3.3 以上



## Composable の基本

よく `useMouse` が例に上がってますね

```ts:useMouse.ts
export const useMouse = () => {
  const x = ref<number>(0)
  const y = ref<number>(0)

  const update = (event: MouseEvent) => {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return {
    x,
    y,
  }
}
```

```vue:Sample.vue
<template>
  <div>{{ mouse }}</div>
</template>

<script setup lang="ts">
const mouse = useMouse()
</script>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/95c41558-ef37-380d-62de-29b311e9c47f.gif)

こんな感じで `ref` や `関数` , `イベント` まで Vue ファイルと全く同じように書けます
その中で public として使いたい値を return してあげる感じです
意外と簡単

- 複数個所で使用する処理
- ネイティブの eventListener を使いたいとき

は Composable を使うと良いです！




## Composable の値の利用方法

`useMouse` のそれぞれの値にアクセスするには、dot記法か分割代入どちらかを使います
お好みで

```vue:Sample.vue
<template>
  <div>(1) {{ mouse.x }}, {{ mouse.y }}</div>
  <div>(2) {{ x }}, {{ y }}</div>
</template>

<script setup lang="ts">
// (1)
const mouse = useMouse()

// (2)
const { x, y } = useMouse()
</script>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/171d780c-60bb-97d9-ae6f-2337a5fba866.gif)

私は `mouse` で prefix を付けるほうが好き

記載はしてませんが、関数でもなんでも return したものをオブジェクトのように呼び出すことが可能です




## Computed の使い方 (`toRef()`)

Composable 内で `Computed` を使う場合はちょっと注意
そのまま呼び出すと `.value` を付与しないといけません

そこで `toRef()` という関数を経由して渡します
これに入れたものは、自動的に ref 属性としてリアクティブに扱われます　便利

```ts:useMouse.ts
export const useMouse = () => {
  // ...

  // mouse の X座標 の半分の値
  const halfX = computed(() => x.value / 2)

  return {
    // (1)
    halfX,
    // (2)
    refHalfX: toRef(halfX),
  }
}
```

```vue:Sample.vue
<template>
  <div>(1) {{ mouse.halfX.value }}</div>
  <div>(2) {{ mouse.refHalfX }}</div>
</template>

<script setup lang="ts">
const mouse = useMouse()
</script>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/fc7b4f64-c0b0-70e2-37cb-fa131868b3f6.gif)

シンプル is ベスト


:::note alert
.value については調査不足かもしれません...
templateの ts 領域で上手く動かない...
:::



## 外部の値を使って処理したい (`MaybeRef`, `unref()`)

特定の値を使った Composable を作りたくなることも多い
ただそのまま引数に渡すと、値の変更を検知してくれません...
ref や computed のようなリアクティブな値を渡すと良いです

その際は `MaybeRef` と `unref()` を利用すると上手くいきます

```ts:useMouse.ts
export const useMouse = (px: MaybeRef<string | undefined>) => {
  // ...

  // mouse の X座標 にデフォルト値を加えたもの
  const fixedX = computed(() => x.value + Number(unref(px) ?? 0))

  return {
    fixedX: toRef(fixedX),
  }
}
```

```vue:Sample.vue
<template>
  <input v-model="px" size="small" />
  <div>{{ mouse.fixedX }}</div>
</template>

<script setup lang="ts">
const px = ref<string>()
const mouse = useMouse(px)
</script>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/17bb6d90-6f6a-5003-cd63-cd44a8690743.gif)

型 `MaybeRef` の値を取り出す際は `toValue()` を使う
内部的には `T | Ref<T>` => `T` みたいなことをやってくれてる
`.value` みたいなものと思えばOK

もしオプションみたいな固定の設定値を渡したいときは、object形式で渡すと良い
キーが補完されるのでコーディングしやすい




## 外部の値を使って処理したい！が何故か値がリアクティブに変わらない (`MaybeRefOrGetter`, `toValue()`)

稀に Props 値を使った場合など、値がリアクティブにならないことがある
そういう時は Getter 式で当てはめればOK

合わせて `MaybeRefOrGetter` と `toValue()` を利用する
これらは先の例の `Getter` 対応版

```ts:useMouse.ts
export const useMouse = (px: MaybeRefOrGetter<string | undefined>) => {
  // ...
}
```

```vue:Sample.vue
<template>
  <div>px: {{ px }}</div>
  <div>{{ mouse.fixedX }}</div>
</template>

<script setup lang="ts">
const props = defineProps<{
  px?: string
}>()

const mouse = useMouse(() => props.px)
</script>
```

```vue:Parent.vue
<template>
  <TestComponent :px="`100`" />
</template>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/56eb0d7a-cd69-a285-6979-a79eb8ca6478.gif)

コード統一のために、常にこっちでも良いかもね

> 何かの実装時に props だと動かないんだと思ってたのに、  
> テストコードではそのまま動いた...  
>  
> 昨今のSPAはリアクティブになる条件が難しいね  




## Composable をそのまま Component に渡したい (`ReturnType<typeof xxx>`)

できるぞ
Composable を `ReturnType` に通すと、型情報が出てくるので利用する

※ 簡略化のため `toMouse` は一番最初のものに戻す

```vue:Sample.vue
<template>
  <div>{{ mouse }}</div>
</template>

<script setup lang="ts">
defineProps<{
  mouse: ReturnType<typeof useMouse>
}>()
</script>
```

```vue:Parent.vue
<template>
  <TestComponent :mouse="mouse" />
</template>

<script setup lang="ts">
const mouse = useMouse()
</script>
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833190/31c13235-a24a-f095-2b9f-d3d862ec1e39.gif)

何も変わらずに使える！
これができると、ロジックのカプセル化に興味がわく人もでるのではないか




## Composable を inject して利用したい (`InjectionKey<ReturnType<typeof xxx>>`)

できるぞ
Props の応用と `InjectionKey` を使って型の鍵を作る
その鍵を使って provide - inject する

※ 簡略化のため `toMouse` は一番最初のものに戻す
※ inject の説明はとても長くなるので省きます

```vue:Sample.vue
<template>
  <div>{{ mouse }}</div>
</template>

<script setup lang="ts">
import { useMouseKey } from '@/Pages/Sample.vue'

const mouse = inject(useMouseKey)
</script>
```

```vue:Parent.vue
<template>
  <TestComponent />
</template>

<script lang="ts">
export const useMouseKey: InjectionKey<ReturnType<typeof useMouse>> = Symbol()
</script>

<script setup lang="ts">
const mouse = useMouse()
provide(useMouseKey, mouse)
</script>
```

キーを使うことで型情報も引っ張ってこれる
script setup でも export できればもう少し簡単に表現できるんだけどね...

inject は革命が起きる一方、global に定義しているようなものなのでスパゲッティになりやすい...
また undefined が有り得るので、気を付けてコーデイングをしよう



## おわりに

Vue3 ってこんなに進化してるの！
これだけできれば、バケツリレーの廃止とか、通信ロジックの隠匿とかできそうじゃない？

個人的にはまだ `shallowRef` とかの使い方が分かってない
とりあえず「リアクティブに反映させたい値は Ref 化する」だけ覚えれば何とかなる

React も嫌いじゃないけど、私は Vue を信仰していくぅ
Vue の書き心地がとても良い！

- [リアクティビティー API: コア \| Vue\.js](https://ja.vuejs.org/api/reactivity-core.html)
- [リアクティビティー API: ユーティリティー \| Vue\.js](https://ja.vuejs.org/api/reactivity-utilities.html)
- [リアクティビティー API: 上級編 \| Vue\.js](https://ja.vuejs.org/api/reactivity-advanced.html)
- [ユーティリティー型 \| Vue\.js](https://ja.vuejs.org/api/utility-types)

理解を深めたい人は `VueUse` ライブラリを覗いてみるとよいかも
大体の事例は既に実装されてる

https://vueuse.org/