Шаблонні посилання у Vue.js
Template refs у Vue.js дають прямий доступ до DOM-елементів або екземплярів дочірніх компонентів з JavaScript.
Теорія
TL;DR
- Template ref - це змінна
ref(), яку Vue заповнює справжнім DOM-вузлом (або екземпляром компонента) після монтування - До монтування значення завжди
null - У
<script setup>дочірні компоненти нічого не відкривають за замовчуванням - потрібенdefineExpose, щоб надати доступ батьківському компоненту - Refs потрібні там, де реактивність Vue не справляється самостійно: фокус, скрол, canvas, ініціалізація сторонніх бібліотек
- Реактивні дані вирішують більшість задач. Refs - для виключень.
Швидкий приклад
<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> не відкривають нічого за замовчуванням. Дочірній компонент повинен явно оголосити, що доступне ззовні:
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() { count.value = 0 }
defineExpose({ count, reset })
</script><!-- 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 стає масивом:
<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() прямо в тілі компонента.
// Неправильно - 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 відразу після монтування.
<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-вузла. Передавати реактивну змінну тут не вийде.
<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 - єдиний чистий спосіб його отримати.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.