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.valueHTMLCanvasElement | 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との組み合わせ

条件付きレンダリングで表示・非表示が切り替わる要素を参照する場合、watchEffectwatch と組み合わせるのが安全です。

<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.valuenull になります。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 を標準的に採用し、既存プロジェクトも段階的に移行することで、テンプレート参照まわりのコードの可読性と保守性が高まります。