Skip to main content

Шаблонні посилання у Vue.js

Template refs у Vue.js дають прямий доступ до DOM-елементів або екземплярів дочірніх компонентів з JavaScript.

Теорія

TL;DR

  • Template ref - це змінна ref(), яку Vue заповнює справжнім DOM-вузлом (або екземпляром компонента) після монтування
  • До монтування значення завжди null
  • У <script setup> дочірні компоненти нічого не відкривають за замовчуванням - потрібен defineExpose, щоб надати доступ батьківському компоненту
  • Refs потрібні там, де реактивність Vue не справляється самостійно: фокус, скрол, canvas, ініціалізація сторонніх бібліотек
  • Реактивні дані вирішують більшість задач. Refs - для виключень.

Швидкий приклад

vue
<script setup> import { ref, onMounted } from 'vue' const inputRef = ref(null) onMounted(() => { // до монтування null, після - HTMLInputElement inputRef.value?.focus() }) </script> <template> <input ref="inputRef" type="text" /> </template>

Рядок "inputRef" у шаблоні відповідає назві змінної в <script setup>. Vue з'єднує їх автоматично.

Коли використовувати template refs

Refs доречні тоді, коли реактивна модель Vue не вирішує задачу самостійно:

  • Сфокусувати поле після монтування або після дії користувача
  • Прокрутити до конкретного елемента (scrollIntoView)
  • Прочитати розміри елемента (getBoundingClientRect, offsetHeight)
  • Малювати на canvas (getContext('2d'))
  • Ініціалізувати сторонню бібліотеку (Chart.js, D3), якій потрібен справжній DOM-вузол

Якщо задача вирішується через v-model, :class або обчислювані дані - краще йти тим шляхом.

Refs у компонентах і defineExpose

Якщо поставити ref на дочірній компонент, отримаєш доступ до його публічного екземпляра. Але компоненти з <script setup> не відкривають нічого за замовчуванням. Дочірній компонент повинен явно оголосити, що доступне ззовні:

vue
<!-- ChildComponent.vue --> <script setup> import { ref } from 'vue' const count = ref(0) function reset() { count.value = 0 } defineExpose({ count, reset }) </script>
vue
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import ChildComponent from './ChildComponent.vue' const childRef = ref(null) function handleReset() { childRef.value?.reset() } </script> <template> <ChildComponent ref="childRef" /> <button @click="handleReset">Скинути</button> </template>

Без defineExpose - childRef.value буде порожнім proxy. Будь-який виклик методу просто нічого не зробить.

Refs всередині v-for

Якщо ref стоїть на елементі всередині v-for, змінна ref стає масивом:

vue
<script setup> import { ref, onMounted } from 'vue' const listRefs = ref([]) onMounted(() => { listRefs.value[0]?.scrollIntoView() }) </script> <template> <li v-for="item in items" :key="item.id" ref="listRefs"> {{ item.name }} </li> </template>

Порядок у масиві відповідає порядку в DOM, а не порядку додавання елементів.

Часті помилки

Звернення до ref.value до монтування. Ref має значення null поза onMounted або пізнішими хуками. Більшість таких помилок - це виклик .focus() прямо в тілі компонента.

js
// Неправильно - ref тут null const inputRef = ref(null) inputRef.value.focus() // TypeError: Cannot read properties of null // Правильно onMounted(() => { inputRef.value?.focus() })

v-if скидає ref. Коли умова v-if стає false, елемент видаляється з DOM і ref повертається до null. При наступному рендері починається новий цикл монтування.

Відсутній defineExpose у дочірньому компоненті. Ставиш ref на компонент, намагаєшся викликати метод - і нічого не відбувається. Потрібен defineExpose({ methodName }) у дочірньому компоненті.

Ручне читання стану DOM замість реактивності. Брати inputRef.value.value для читання тексту поля і оновлювати його вручну - для цього є v-model.

Питання для співбесіди

Q: Чому template ref дорівнює null до монтування?
A: Vue спочатку будує дерево virtual DOM, а потім застосовує його до реального DOM під час фази монтування. До цього моменту реального елемента ще не існує.

Q: Яка різниця між ref="inputRef" і :ref="fn"?
A: Рядкова версія прив'язується до змінної inputRef у <script setup>. Прив'язана версія приймає функцію, яка отримує елемент напряму - корисно для зберігання кількох refs у Map.

Q: Чи можна використовувати template refs з Options API?
A: Так. В Options API доступ до refs через this.$refs.inputRef. Поведінка та сама - null до монтування, заповнюється після.

Q: Що відбувається з ref всередині v-if, коли умова змінюється?
A: Коли умова стає false, Vue демонтує елемент і встановлює ref в null. Коли умова повертається до true, Vue монтує новий елемент і знову заповнює ref.

Приклади

Фокус на полі форми після монтування

Форма входу, яка фокусує поле email відразу після монтування.

vue
<script setup> import { ref, onMounted } from 'vue' const emailRef = ref(null) onMounted(() => { emailRef.value?.focus() }) </script> <template> <form> <input ref="emailRef" type="email" placeholder="Email" /> <input type="password" placeholder="Пароль" /> <button type="submit">Увійти</button> </form> </template>

Опціональний ланцюжок ?. захищає від рідкісного випадку, коли компонент демонтується до спрацювання onMounted.

Ініціалізація графіка через canvas ref

Сторонні бібліотеки на кшталт Chart.js потребують справжнього DOM-вузла. Передавати реактивну змінну тут не вийде.

vue
<script setup> import { ref, onMounted, onUnmounted } from 'vue' import Chart from 'chart.js/auto' const canvasRef = ref(null) let chart = null onMounted(() => { if (!canvasRef.value) return chart = new Chart(canvasRef.value, { type: 'bar', data: { labels: ['Січ', 'Лют', 'Бер'], datasets: [{ label: 'Продажі', data: [12, 19, 8] }] } }) }) onUnmounted(() => { chart?.destroy() // щоб уникнути витоків пам'яті }) </script> <template> <canvas ref="canvasRef" width="400" height="200" /> </template>

Команди часто намагаються підключити Chart.js через реактивний стан. Так не виходить. Canvas потребує справжнього вузла, і template ref - єдиний чистий спосіб його отримати.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?