Vue.js でテンプレート内の DOM 要素や子コンポーネントの参照は、旧来の Options API だと this.$refs
で取得できました。
では、Composition API ではどうなっているのでしょうか。
答えは 公式サイトに書いてあります。
<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 で作ったプロジェクトをベースにして検証しました。1
DOM 要素の場合
<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>
-
ref
関数は引数なしの場合、ジェネリクスで指定した型と undefined のユニオンになります。2
上記の例だとimgRef
の型はRef<HTMLImageElement | undefined>
です。 - レンダリング前は undefined です。
- レンダリング後だと img 要素の参照が取得できます。
- この時点では画像データはまだ読み込まれていないので高さは 0 です。
また、imgRef.value
はHTMLImageElement | undefined
型なので、?.
を使用しています。気になる場合は関数の先頭でif (!imgRef.value) return
などを書いて、undefined の可能性を除外するとよいでしょう。 - 画像が読み込まれると、正しいサイズが取得できます。
コンポーネントの場合
<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>
- コンポーネントの実態はコンストラクタなので、このような型になります。
- インスタンスの Proxy オブジェクトが入っています。
- Vue 3 では $el はテキストノード(
<template>
開きタグと最初の DOM 要素の間)になります。3 - props のプロパティが直接生えており、値が取得できます(
componentRef.value.$props.msg
でも同じ) - data も同様です(
componentRef.value.$data.count
でも同じ) - 外から data の値を変えることもできてしまいますが、メンテナンス性を著しく損なうのでやめましょう。
まとめ
以前にくらべると少し手間は増えましたが、型の恩恵を受けるためなので仕方ないですね。4
今までコンポーネント ref はほとんど使っていませんでした。唯一使いそうな $el
がテキストノードに変更されてしまい、さらに使いどころが分からなくなりました。誰か教えて下さい。
-
参考になるか分かりませんが、リポジトリはこちら https://github.com/jay-es/composition-api-refs ↩
-
tsconfig.json
で"strictNullChecks": true
(もしくは"strict": true
)にしている場合。
false になっていると| undefined
にはなりません。 ↩ -
Vue 2 +
@vue/composition-api
の頃はコンポーネント全体の DOM 要素が取得できました。
Vue 3 でテンプレートのルートに複数の要素を置けるようになったことが関係していると思われます。 ↩ -
実は Vue 2 +
@vue/composition-api
の場合、setup
関数の第二引数にrefs
が生えています。なんとなく理由は分かりますよね。
ただし、型情報には存在しないためas
などでごまかさないといけないのでオススメできません。 ↩