はじめに
先日Vue3.5(Tengen Toppa Gurren Lagann)が正式にリリースされました。
3.5では、リアクティブシステムの最適化やSSRのパフォーマンス改善などが行われています。
本記事では3.5の新機能のうち、useTemplateRef
に焦点を当て、従来の手法と比較しながらその利点を解説します。
useTemplateRefとは
useTemplateRefはテンプレート内のDOM要素を取得して操作するための方法をcomposableの形で提供している機能です。
取得したいDOM要素にref属性を設定し、設定した値をuseTemplateRefの引数に渡すことで参照したい要素を取得することができます。
<script setup>
import { useTemplateRef } from 'vue'
const myDiv = useTemplateRef('myDiv')
</script>
<template>
<div ref="myDiv">This is my div</div>
</template>
Vue3.5以前のやり方
Vue3.5以前もDOM要素の取得自体は行えましたが、ref
を利用して取得する必要がありました。
ref
の変数名を取得したいDOM要素のref属性の値と一致させることで、要素にアクセスすることができます。
<script setup>
import { ref, onMounted } from 'vue'
const myDiv = ref(null)
onMounted(() => {
console.log(myDiv.value) // DOM要素にアクセス
})
</script>
<template>
<div ref="myDiv">This is my div</div>
</template>
refで要素を取得することの問題点
ref
でDOM要素を取得する場合にいくつか問題点がありました。
初見だと分かりづらい
暗黙的に変数名を用いて取得するDOM要素を特定しているため、初めてこの機能を利用する場合に混乱しやすい状態でした。
知らずに変数名を変えてしまい、DOM要素が取得できずに困った人もいるのではないでしょうか。
composableに切り出しづらい
ref
でDOM要素を取得する場合、必然的に変数名を固定化する必要があります。
これにより、DOM要素に何らかの操作をする処理をcomposableとして切り出したい場合に要素の取得処理自体はcomposableに切り出すことができなくなっていました。
// DOM要素にアクセスして何かするcomposable
import { ref, onMounted } from 'vue'
export function useElementAction(elementRef) {
const doSomethingWithElement = () => {
console.log(elementRef.value)
elementRef.value.style.color = 'blue' // 要素の色を変更するなど
}
onMounted(() => {
if (elementRef.value) {
doSomethingWithElement()
}
})
return {
doSomethingWithElement
}
}
<script setup>
import { ref } from 'vue'
import { useElementAction } from './useElementAction'
// DOM要素の取得自体はcomposableを使う側で行う必要がある
const myDiv = ref(null)
// 引数でDOM要素自体を渡す
useElementAction(myDiv)
</script>
<template>
<div ref="myDiv">This is my div</div>
</template>
useTemplateRefでどう改善されたか
取得したい要素のrefの値をuseTemplateRef
に渡すだけでよくなったため、要素の取得処理自体もcomposableに切り出せるようになりました。
// DOM要素にアクセスして何かするcomposable
import { useTemplateRef, onMounted } from 'vue'
export function useElementAction(refKey) {
// DOM要素の取得処理もcomposable側に寄せられる!
const elementRef = useTemplateRef(refKey)
const doSomethingWithElement = () => {
console.log(elementRef.value)
elementRef.value.style.color = 'blue' // 要素の色を変更するなど
}
onMounted(() => {
if (elementRef.value) {
doSomethingWithElement()
}
})
return {
doSomethingWithElement
}
}
<script setup>
import { useElementAction } from './useElementAction'
// 操作したいDOM要素のrefの値を文字列渡すだけでいい
useElementAction('myDiv')
</script>
<template>
<div ref="myDiv">This is my div</div>
</template>
(おまけ)どうやって実装されているか
せっかくなので、useTemplateRefがどのように実装されているかコードを見てみました。
(実際のコードはこちら)
まず現在のコンポーネントのインスタンスを取得し、refsプロパティを取得(存在しない場合は初期化)しています。
const i = getCurrentInstance()
const r = shallowRef(null)
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
// ...省略
}
次にObject.definePropertyを利用し、useTemplateRefの引数で渡された文字列(key)でプロパティを定義します。
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: val => (r.value = val),
})
これにより、templateのレンダリング時に対象のDOM要素がリアクティブな変数に代入されるようになります。
最後にこのリアクティブな変数を戻り値として返すことで、DOM要素をリアクティブな値として取得できるようになっています。