Vue 3のComposition APIでDOM要素を参照するとき、ref(null) を使って変数名とテンプレートの ref 属性を一致させる方法が主流でした。しかしこの方法には「変数名の制約」「composableへの切り出しが困難」「TypeScriptの型推論が効かない」といった課題があります。Vue 3.5で追加された useTemplateRef() は、これらの問題を解消するComposition APIヘルパー関数です。
useTemplateRefの概要とAPIシグネチャ
useTemplateRef() は、テンプレート内の ref 属性と紐づくDOM要素やコンポーネントインスタンスを取得するための関数です。Vue 3.5(2024年9月リリース)で導入されました。
関数シグネチャ:
function useTemplateRef<T>(key: string): Readonly<ShallowRef<T | null>>
引数 key にテンプレート側の ref 属性値を文字列として渡すと、対応する要素への参照を保持する ShallowRef が返されます。マウント前は null で、マウント後に実際のDOM要素またはコンポーネントインスタンスがセットされます。
基本的な使い方
入力フィールドにマウント直後にフォーカスを当てる例です。
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef('my-input')
onMounted(() => {
inputRef.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>
ポイントは2つあります。
- 変数名と
ref属性値が別でよい:inputRefという変数名に対し、テンプレートではref="my-input"と命名できます - 文字列ベースの紐づけ:
useTemplateRef('my-input')の引数文字列がテンプレート側のref属性値と一致していれば動作します
従来のref()との比較
Vue 3.5より前は、ref(null) を使ったテンプレート参照が唯一の方法でした。
<script setup>
import { ref, onMounted } from 'vue'
// 変数名「input」がテンプレートの ref="input" と一致する必要がある
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
両者の違いを整理します。
| 観点 | ref(null) | useTemplateRef() |
|---|---|---|
| 導入バージョン | Vue 3.0〜 | Vue 3.5〜 |
| 変数名の制約 | ref属性値と同一名が必須 | 自由に命名可能 |
| composable化 | 困難(変数名がテンプレートに依存) | 容易(文字列キーで参照先を指定) |
| TypeScript型推論 | 手動でジェネリクス指定が必要 | 自動推論が可能 |
| 戻り値 | Ref<T | null> | Readonly<ShallowRef<T | null>> |
| 可読性 | 初見では「リアクティブ変数」と区別しにくい | テンプレート参照だと明示的 |
ref(null) は現在も動作しますが、Vue公式ドキュメントでは3.5以降のプロジェクトに useTemplateRef() の使用を推奨しています。
TypeScriptでの型付け
自動型推論
Vue Language Tools(Volar)環境では、useTemplateRef() で取得した参照の型がテンプレートの要素に基づいて自動推論されます。例えば <input ref="my-input" /> に対応する useTemplateRef('my-input') は、自動的に HTMLInputElement 型として推論されます。
ジェネリクスによる明示的な型指定
自動推論が機能しない場面や、より明確に型を指定したい場合はジェネリクスを使います。
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
これにより canvasRef.value は HTMLCanvasElement | null 型となり、Canvas APIのメソッドが補完されます。
コンポーネント参照の型
子コンポーネントを参照する場合は InstanceType を組み合わせます。
import { useTemplateRef } from 'vue'
import MyDialog from './MyDialog.vue'
type MyDialogInstance = InstanceType<typeof MyDialog>
const dialogRef = useTemplateRef<MyDialogInstance>('dialog')
ジェネリックコンポーネントを扱う場合は vue-component-type-helpers ライブラリの ComponentExposed を利用します。
import type { ComponentExposed } from 'vue-component-type-helpers'
import MyGenericModal from './MyGenericModal.vue'
const modalRef = useTemplateRef<ComponentExposed<typeof MyGenericModal>>('modal')
v-forで複数要素を参照する
v-for の中で ref 属性を指定すると、マウント後に対応する要素の配列が取得できます。
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const items = ref(['Apple', 'Banana', 'Cherry'])
const itemRefs = useTemplateRef('item-el')
onMounted(() => {
// itemRefs.value は HTMLLIElement[] 型
console.log(itemRefs.value)
itemRefs.value.forEach(el => {
console.log(el.textContent)
})
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item" ref="item-el">
{{ item }}
</li>
</ul>
</template>
注意点として、ref 配列はソースの配列と同じ順序を保証しません。要素の順序に依存するロジックでは、別途インデックス管理が必要です。
v-ifとの組み合わせ
条件付きレンダリングで表示・非表示が切り替わる要素を参照する場合、watchEffect や watch と組み合わせるのが安全です。
<script setup>
import { ref, useTemplateRef, watchEffect } from 'vue'
const isVisible = ref(false)
const panelRef = useTemplateRef('panel')
watchEffect(() => {
if (panelRef.value) {
// パネルがDOMにマウントされた時点で実行される
panelRef.value.scrollIntoView({ behavior: 'smooth' })
}
})
</script>
<template>
<button @click="isVisible = !isVisible">切り替え</button>
<div v-if="isVisible" ref="panel">
条件付きコンテンツ
</div>
</template>
v-if="false" のときは panelRef.value が null になります。onMounted 内でアクセスしても、マウント時点で非表示の要素は null のままです。watchEffect を使えば、表示された瞬間を検知して処理を実行できます。
composableへの切り出し
useTemplateRef() の大きな利点は、DOM操作ロジックをcomposable(カスタムフック)として分離できることです。従来の ref(null) では変数名がテンプレートに依存するため、composable内で完結しにくい構造でした。
例: オートフォーカスcomposable
// composables/useAutoFocus.ts
import { useTemplateRef, onMounted } from 'vue'
export function useAutoFocus(refKey: string) {
const el = useTemplateRef<HTMLElement>(refKey)
onMounted(() => {
el.value?.focus()
})
return { el }
}
<script setup>
import { useAutoFocus } from '@/composables/useAutoFocus'
useAutoFocus('search-input')
</script>
<template>
<input ref="search-input" placeholder="検索..." />
</template>
例: スクロール位置の監視composable
// composables/useScrollObserver.ts
import { useTemplateRef, onMounted, onUnmounted, ref } from 'vue'
export function useScrollObserver(refKey: string) {
const el = useTemplateRef<HTMLElement>(refKey)
const scrollTop = ref(0)
function handleScroll() {
if (el.value) {
scrollTop.value = el.value.scrollTop
}
}
onMounted(() => {
el.value?.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
el.value?.removeEventListener('scroll', handleScroll)
})
return { scrollTop }
}
呼び出し側は ref 属性のキー文字列を渡すだけで、DOM操作の詳細を意識する必要がありません。
コンポーネント参照とdefineExpose
子コンポーネントに ref を設定すると、コンポーネントインスタンスへの参照が取得できます。
<!-- Parent.vue -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import ChildForm from './ChildForm.vue'
const formRef = useTemplateRef('child-form')
onMounted(() => {
// ChildFormが公開したメソッドを呼び出す
formRef.value?.validate()
})
</script>
<template>
<ChildForm ref="child-form" />
</template>
<script setup> を使うコンポーネントはデフォルトで全てのプロパティ・メソッドがプライベートです。親から呼び出したいメンバーは defineExpose で明示的に公開する必要があります。
<!-- ChildForm.vue -->
<script setup>
import { ref } from 'vue'
const formData = ref({ name: '', email: '' })
function validate() {
// バリデーションロジック
return formData.value.name.length > 0
}
function reset() {
formData.value = { name: '', email: '' }
}
// 親コンポーネントから呼び出せるメソッドを公開
defineExpose({ validate, reset })
</script>
defineExpose で公開されていないメンバーは、親から ref 経由でアクセスできません。これはコンポーネントのカプセル化を保つ設計意図によるものです。
よくあるトラブルと対策
useTemplateRefの値がnullになる
最も多いトラブルです。主な原因は3つあります。
原因1: マウント前にアクセスしている
const el = useTemplateRef('target')
// NG: setup時点ではまだDOMが存在しない
console.log(el.value) // null
onMounted(() => {
// OK: マウント後なら値が入っている
console.log(el.value)
})
原因2: ref属性のキー文字列が一致していない
// script側
const el = useTemplateRef('myInput')
<!-- template側: キー名が違う -->
<input ref="my-input" /> <!-- 'my-input' ≠ 'myInput' -->
useTemplateRef の引数とテンプレートの ref 属性値は完全一致が必要です。ケバブケースとキャメルケースの混在に注意してください。
原因3: v-ifで条件付きレンダリングされている
要素が v-if="false" で非表示のとき、DOMに存在しないため null になります。前述の watchEffect パターンで対処できます。
useTemplateRef is not a function
Vue 3.5未満のバージョンで useTemplateRef をインポートしようとした場合に発生します。
# Vueのバージョン確認
npm list vue
Vue 3.5以上であることを確認してください。3.5未満の場合は従来の ref(null) パターンを使用するか、Vueをアップデートしてください。
npm install vue@latest
TypeScriptで「Object is possibly null」エラー
useTemplateRef の戻り値は T | null 型のため、.value のプロパティアクセス時にnullチェックが必要です。
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
// NG: null の可能性がある
// inputRef.value.focus()
// OK: オプショナルチェーン
inputRef.value?.focus()
// OK: nullガード
if (inputRef.value) {
inputRef.value.focus()
}
})
computedと組み合わせた際のTypeScriptエラー
useTemplateRef の値を computed で参照し、そのcomputedをテンプレートで使用すると、vue-tscで型エラーが発生するケースが報告されています(vuejs/language-tools#5022)。Vue Language Toolsのアップデートで段階的に修正されているため、最新バージョンへの更新が有効です。
関数refとの使い分け
テンプレート参照には useTemplateRef の他に、関数refという方法もあります。
<template>
<input :ref="(el) => { /* el を変数に保存 */ }" />
</template>
関数refはコンポーネントの更新ごとに呼び出され、要素がアンマウントされると引数が null になります。動的に参照先を切り替えたい場合や、参照取得時に追加処理を挟みたい場合に適しています。
| ユースケース | 推奨方法 |
|---|---|
| 通常のDOM要素参照 | useTemplateRef |
| composableへの切り出し | useTemplateRef |
| 参照取得時にコールバックが必要 | 関数ref |
| 動的に参照先を切り替えたい | 関数ref |
| Vue 3.5未満のプロジェクト | ref(null) |
Nuxtプロジェクトでの利用
Nuxt 3はVue 3ベースのため、Nuxtのバンドルに含まれるVueが3.5以上であれば useTemplateRef を利用できます。
# Nuxtが使用するVueバージョンの確認
npx nuxi info
Nuxt固有の注意点として、SSR(サーバーサイドレンダリング)環境ではサーバー側でDOMが存在しません。useTemplateRef の値はサーバー側では常に null です。DOM操作は必ず onMounted 内で行ってください。
<ClientOnly> コンポーネント内で使用する場合も同様に、onMounted フック内でのアクセスが安全です。
ref(null)からuseTemplateRefへの移行手順
既存プロジェクトを useTemplateRef に移行する手順は以下の通りです。
Step 1: Vueバージョンの確認・更新
npm install vue@^3.5.0
Step 2: インポート文の変更
- import { ref, onMounted } from 'vue'
+ import { useTemplateRef, onMounted } from 'vue'
Step 3: ref(null)をuseTemplateRefに置換
- const input = ref(null)
+ const inputRef = useTemplateRef('input')
テンプレート側の ref 属性値と useTemplateRef の引数を一致させます。変数名はテンプレートに合わせる必要がなくなるため、 Ref サフィックスを付けるなど分かりやすい命名に変更できます。
Step 4: テンプレートの確認
テンプレート側の ref 属性値は変更不要です。ただし、変数名の変更に伴いテンプレート内で ref 変数を直接参照している箇所がないか確認してください。
移行は段階的に進められます。ref(null) と useTemplateRef は同一コンポーネント内で共存できるため、ファイルごとに少しずつ置き換えていくアプローチが現実的です。
まとめ
useTemplateRef() はVue 3.5で追加されたComposition APIヘルパーで、テンプレート内のDOM要素やコンポーネントインスタンスへの参照を安全かつ明示的に取得できます。従来の ref(null) パターンと比べて、変数名の自由度・composable化のしやすさ・TypeScript型推論の精度が向上しています。
新規のVue 3.5以降のプロジェクトでは useTemplateRef を標準的に採用し、既存プロジェクトも段階的に移行することで、テンプレート参照まわりのコードの可読性と保守性が高まります。
