Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Телепортація у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Teleport** - вбудований компонент Vue 3, який рендерить контент у будь-який DOM-вузол поза деревом компонентів. ```vue <Teleport to="body"> <div v-if="show" class="modal">Контент модалки</div> </Teleport> ``` **Ключове:** DOM переміщується до цільового елемента, але реактивність, пропси та події залишаються прив'язаними до оригінального компонента.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Teleport** - це вбудований компонент Vue 3, який рендерить контент у будь-який DOM-вузол поза деревом компонентів. ## Теорія ### TL;DR - Teleport схожий на пряму доставку: контент пишеш в одному місці, Vue відносить DOM до потрібної адреси - Контент переїжджає до `to`-цілі, але залишається під управлінням Vue (реактивність, пропси, події, provide/inject) - Дерево компонентів Vue не змінюється, змінюється тільки позиція в DOM - Використовуй коли батьківський CSS (overflow, z-index stacking context) ламає перекриваючі елементи - Пропускай коли `position: fixed` вирішує задачу без конфліктів стилів ### Швидкий приклад ```vue <template> <button @click="show = true">Відкрити</button> <Teleport to="body"> <div v-if="show" class="modal" @click.self="show = false"> Клік за межами закриє вікно </div> </Teleport> </template> <script setup> import { ref } from 'vue' const show = ref(false) </script> <style scoped> .modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); } </style> ``` Модальне вікно рендериться як прямий нащадок `<body>`, а не всередині батьківського компонента. При цьому `v-if` і обробник кліку реагують на `show` з оригінального скоупу компонента. ### Ключова різниця Teleport переміщує реальний DOM до `to`-селектора під час монтування. Інстанс компонента і реактивність залишаються в оригінальному дереві Vue. Тобто `v-model`, пропси, події та provide/inject працюють як звичайно. CSS-успадкування починається від нового батьківського елемента, а не від оригінального, що звільняє телепортований контент від стилів батька. ### Коли використовувати - **Модальні вікна і діалоги:** `to="body"` вирішує проблеми z-index, які виникають через батьківські stacking context - **Тултіпи і поповери:** виходить за межі `overflow: hidden` в контейнерах-предках - **Сповіщення:** `to="#notifications"` складає їх без впливу на потік лейауту - **Дропдауни в rich-text редакторах:** не обрізаються контейнером редактора Переважна більшість z-index проблем у Vue-дашбордах трапляється через модалки всередині карточок з `overflow: hidden`. Один `<Teleport to="body">` вирішує це за секунду. Якщо `position: fixed` справляється без конфліктів - Teleport не потрібен. ### Як Vue обробляє Teleport всередині Під час монтування Vue-рендерер витягує дочірні VNode з Teleport, викликає `document.querySelector(to)` щоб знайти ціль, і додає реальний DOM через `appendChild` або `insertBefore`. Оновлення реактивності все одно потрапляють до переміщених вузлів через внутрішній прапор vnode. При розмонтуванні Vue автоматично видаляє DOM з цілі. Якщо `:disabled="true"` - переміщення не відбувається, контент рендериться на місці. ### Часті помилки **1. Телепортація до селектора який не існує** ```vue <!-- Неправильно: контент стає невидимим --> <Teleport to=".missing-class"> <div class="modal">...</div> </Teleport> ``` `querySelector` повертає null, контент рендериться inline але може бути повністю невидимим. Використовуй `body` або гарантовано існуючий елемент. Це найпоширеніша помилка з Teleport на StackOverflow. **2. Очікування що scoped-стилі підуть за телепортованим контентом** ```vue <!-- Неправильно: стиль .modal не спрацює після телепортації --> <Teleport to="body"> <div class="modal">...</div> </Teleport> <style scoped> .modal { opacity: 1; } /* CSS залишається прив'язаним до оригінального місця монтування */ </style> ``` Scoped-стилі додають атрибут `data-v-xxxxx` до DOM-вузла і CSS-селектора. Телепортований вузол зберігає атрибут, але стилі на рівні body можуть перекривати твої. Рішення: глобальний блок `<style>` або `:deep()` для всього що має застосовуватись після переміщення. **3. Ігнорування SSR** На сервері немає `body` для пошуку. Vue рендерить контент Teleport inline під час SSR. Якщо клієнт потім телепортує, виникає помилка гідратації (hydration mismatch). Вимикай Teleport до завершення монтування: ```vue <script setup> import { ref, onMounted } from 'vue' const isMounted = ref(false) onMounted(() => { isMounted.value = true }) </script> <template> <Teleport to="body" :disabled="!isMounted"> <div class="toast">Збережено!</div> </Teleport> </template> ``` **4. Вкладені Teleport без disabled в SSR** Внутрішній Teleport визначає `to`-селектор відносно DOM-цілі зовнішнього Teleport, а не оригінальної позиції в шаблоні. В SSR це призводить до помилок гідратації. Вимикай внутрішні Teleport на сервері так само як описано вище. ### Де зустрічається в реальних проектах - **Vuetify 3:** всі `v-dialog` і `v-menu` використовують `<Teleport to="body">` - **Element Plus:** `ElMessage` і `ElNotification` телепортуються до кастомного `#app`-кореня - **Naive UI:** `n-message` додається напряму до `document.body` - **Quasar:** `QDialog` під капотом використовує body як ціль У React є `createPortal` для тієї ж задачі, але він потребує ручного очищення при розмонтуванні. Vue Teleport видаляє DOM автоматично. ### Запитання на співбесіді **Q:** Яка різниця між Teleport і `position: fixed`? **A:** `position: fixed` все одно залежить від батьківського stacking context якщо батько має `transform`, `filter` або `will-change`. Teleport фізично переміщує DOM до цільового елемента, тому залежить тільки від його контексту накладання. **Q:** Як Teleport поводиться якщо `to`-селектор відповідає кільком елементам? **A:** Додає до першого елемента з `querySelector`. Використовуй унікальні ID для передбачуваного розміщення. **Q:** Що відбувається під час SSR? **A:** Vue рендерить контент Teleport inline на сервері. На клієнті переміщує DOM під час монтування. Прив'яжи `:disabled` до прапора з `onMounted` щоб уникнути помилок гідратації. **Q:** Чи впливає Teleport на продуктивність? **A:** Мінімально. Кожен Teleport виконує одне переміщення DOM при монтуванні. Уникай створення сотень окремих Teleport - краще об'єднуй сповіщення в один спільний контейнер. **Q:** (Senior) Чому вкладений Teleport визначає `to` відносно цілі зовнішнього Teleport? **A:** `to` внутрішнього Teleport обчислюється відносно живого DOM в момент монтування. Оскільки зовнішній Teleport вже перемістив своє піддерево, внутрішній резолвиться відносно того переміщеного DOM, а не оригінальної позиції в дереві компонентів. ## Приклади ### Базовий: модальне вікно що виходить за межі контейнера ```vue <template> <div class="card" style="overflow: hidden; position: relative;"> <button @click="open = true">Редагувати</button> <Teleport to="body"> <div v-if="open" class="overlay" @click.self="open = false"> <div class="dialog"> <h2>Редагування</h2> <button @click="open = false">Закрити</button> </div> </div> </Teleport> </div> </template> <script setup> import { ref } from 'vue' const open = ref(false) </script> <style> .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; } .dialog { background: white; padding: 24px; border-radius: 8px; } </style> ``` Без Teleport `overflow: hidden` на `.card` повністю обріже модальне вікно. Teleport переміщує діалог до `<body>` і він рендериться коректно. Стиль тут глобальний, не scoped, бо після переміщення scoped-стилі не допомагають. ### Середній: тост-сповіщення з захистом від SSR ```vue <!-- NotificationToast.vue --> <template> <Teleport to="#notifications" :disabled="!mounted"> <Transition name="fade"> <div v-if="visible" class="toast" role="alert">{{ message }}</div> </Transition> </Teleport> </template> <script setup> import { ref, onMounted } from 'vue' defineProps({ message: String }) const visible = ref(true) const mounted = ref(false) onMounted(() => { mounted.value = true setTimeout(() => { visible.value = false }, 3000) }) </script> ``` ```html <!-- index.html --> <div id="app"></div> <div id="notifications"></div> ``` Кілька тостів можуть телепортуватись до одного `#notifications` і складатись у порядку монтування. `Transition` працює коректно бо Vue керує станом анімації на рівні компонента, а не переміщеного DOM. Прапор `mounted` запобігає помилкам гідратації в SSR. ### Просунутий: перевикористовуваний Modal з правильним lifecycle ```vue <!-- Modal.vue --> <template> <Teleport to="body" :disabled="!isMounted"> <Transition name="fade"> <div v-if="isOpen" class="modal-overlay" @click.self="emit('close')"> <div class="modal-content" role="dialog"> <button class="close-btn" @click="emit('close')">x</button> <slot /> </div> </div> </Transition> </Teleport> </template> <script setup> import { ref, onMounted } from 'vue' defineProps({ isOpen: Boolean }) const emit = defineEmits(['close']) const isMounted = ref(false) onMounted(() => { isMounted.value = true }) </script> <style> .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: white; padding: 24px; border-radius: 8px; max-width: 500px; position: relative; } .close-btn { position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 20px; cursor: pointer; } </style> ``` Цей патерн закриває три проблеми які часто зустрічаються в продакшені. Teleport виходить за межі батьківських `overflow` і z-index. `Transition` анімується правильно бо Vue відстежує стан анімації на рівні компонента. А `:disabled="!isMounted"` синхронізує SSR і клієнтський рендеринг. Саме такий підхід використовує Vuetify 3 для всіх `v-dialog`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.