Vue.js の Composition API における this.$refs の取得方法

Vue.js でテンプレート内の DOM 要素や子コンポーネントの参照は、旧来の Options API だと `this.$refs` で取得できました。
では、Composition API ではどうなっているのでしょうか。


答えは [公式サイト](https://vue-composition-api-rfc.netlify.app/api.html#template-refs)に書いてあります。

```vue:公式サイトより引用
<template>
  <div ref="root"></div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // the DOM element will be assigned to the ref after initial render
        console.log(root.value) // <div/>
      })

      return {
        root
      }
    }
  }
</script>
```

`ref` 関数で作った変数（上記の場合は `root`）をテンプレート内の要素の ref 属性に与えます。
初回レンダリング後（`onMounted` のタイミング）、変数の `value` プロパティに DOM 要素の参照が代入される、という流れです。

## TypeScript で型をつける
さて、このまま終わってしまうのも味気ないので TypeScript ではどう書くのか見ていきましょう。
[Vite](https://github.com/vitejs/vite) で作ったプロジェクトをベースにして検証しました。[^rep]

[^rep]: 参考になるか分かりませんが、リポジトリはこちら https://github.com/jay-es/composition-api-refs

### DOM 要素の場合

```vue
<template>
  <img ref="imgRef" alt="Vue logo" src="./assets/logo.png" />
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'

export default defineComponent({
  setup() {
    const imgRef = ref<HTMLImageElement>() // 1: 型を指定

    console.log(imgRef.value) // 2: undefined が出力される

    onMounted(() => {
      console.log(imgRef.value) // 3: <img>
      console.log(imgRef.value?.clientHeight) // 4: 0 が出力される

      imgRef.value?.addEventListener('load', () => {
        console.log(imgRef.value?.clientHeight) // 5: 200 が出力される
      })
    })

    return { imgRef }
  },
})
</script>
```

1. `ref` 関数は引数なしの場合、ジェネリクスで指定した型と undefined のユニオンになります。[^strict]  
  上記の例だと `imgRef` の型は `Ref<HTMLImageElement | undefined>` です。
2. レンダリング前は undefined です。
3. レンダリング後だと img 要素の参照が取得できます。
4. この時点では画像データはまだ読み込まれていないので高さは 0 です。  
  また、`imgRef.value` は `HTMLImageElement | undefined` 型なので、`?.` を使用しています。気になる場合は関数の先頭で `if (!imgRef.value) return` などを書いて、undefined の可能性を除外するとよいでしょう。
5. 画像が読み込まれると、正しいサイズが取得できます。

[^strict]: `tsconfig.json` で `"strictNullChecks": true` (もしくは `"strict": true`)にしている場合。<br>false になっていると `| undefined` にはなりません。

### コンポーネントの場合
```vue
<template>
  <HelloWorld ref="componentRef" msg="Hello Vue 3.0 + Vite" />
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export default defineComponent({
  components: {
    HelloWorld,
  },
  setup() {
    const componentRef = ref<InstanceType<typeof HelloWorld>>() // 1: 型を指定

    onMounted(() => {
      if (!componentRef.value) return // 型から undefined をなくす

      console.log(componentRef.value) // 2: Proxy {…}

      console.log(componentRef.value.$el) // 3: #text

      console.log(componentRef.value.msg) // 4: Hello Vue 3.0 + Vite
      console.log(componentRef.value.count) // 5: 0

      componentRef.value.count++ // 6: バッドプラクティス！
      console.log(componentRef.value.count) // 1 （増えている）
    })

    return { componentRef }
  },
})
</script>
```

1. コンポーネントの実態はコンストラクタなので、このような型になります。
2. インスタンスの Proxy オブジェクトが入っています。
3. Vue 3 では $el はテキストノード（`<template>` 開きタグと最初の DOM 要素の間）になります。[^el]
3. props のプロパティが直接生えており、値が取得できます（`componentRef.value.$props.msg` でも同じ）
4. data も同様です（`componentRef.value.$data.count` でも同じ）
5. 外から data の値を変えることもできてしまいますが、メンテナンス性を著しく損なうのでやめましょう。


[^el]: Vue 2 + `@vue/composition-api` の頃はコンポーネント全体の DOM 要素が取得できました。<br>Vue 3 でテンプレートのルートに複数の要素を置けるようになったことが関係していると思われます。

# まとめ
以前にくらべると少し手間は増えましたが、型の恩恵を受けるためなので仕方ないですね。[^v2]

今までコンポーネント ref はほとんど使っていませんでした。唯一使いそうな `$el` がテキストノードに変更されてしまい、さらに使いどころが分からなくなりました。誰か教えて下さい。


[^v2]: 実は Vue 2 + `@vue/composition-api` の場合、`setup` 関数の第二引数に `refs` が生えています。なんとなく理由は分かりますよね。<br>ただし、型情報には存在しないため `as` などでごまかさないといけないのでオススメできません。