Skip to content

Teleport breaks template reactivity - DOM never re-renders despite reactive state updating correctly [BUG] #14100

@heiko1234

Description

@heiko1234

Vue version

3.5.18

Link to minimal reproduction

https://sfc.vuejs.org/#eNqVVm1v2zYQ/isEi6EJYMuO7aRpCyRAsQ/9UIzpgGFYP9AUZXFliSpJOU2M/vedSNqOm6Qv/WJJR/Lu4Z3vHm9ao9tGQ1NrW+kGyOTSgLYNmIVtKgXPnSk1NFprq1doFJmNUVswa0Bhw1YabOO4dRpcY2HdaG2sZmFpo6C2bFvBSmsDZa2whdK6Bjc3K+qGbdrWaN2suZVcoVH0Zy0bQxbQSAvO8BXLemMh12htjNWwbDS0SoPetGCtrRU8W0A3DdRKQaVhVWvdamOFBm1atm4NZS0UtYK6AaVZqJSFTavJsFZcN0pBbrQCa2Ct0bDSCtZa8wKqBiy0UisLayY0mrqWWqE2tF9joVS6NmAqTSu4g1xpBa3WoMjQKPpba1vbxijQleLdmHWt1caAWrNuK+g6xdu1hUYrq8C2WivwPSittKZYKliSQbLOtN6geeEMKWOqlQKzyUlbXbdaM0KtXaNqC6YxoPFx0zT0oHFdy6qxbJ1Bba0FI/FaCw1GK/bSaOZUCkMBjCl4oTeocK21JsUKjSXFZKYxEixLp6FSCta1BtNoeFYrlpqC+hVUNVsF3LdtwbvVUDOjFRQKVrWGSsFKK22tqRW0WrGPCnIFRu1rBU2toKo1tJZdKvDWaBZ02aZWa2grDa1WcKPFV1CDQ42RUNaaVGvI6xpcZeDZWpZm0FhZK0MLqJSFplZQKfLDV1OztmgrBde1Mra2sLKwUs5ACyulYKXhpdaE5/xca1hbQyGwNBdQ1OBqo0hpNBSV5r21omWlqBWYTas1K0bh2aqFXGsoK00LRr2mBdK1IlVBS2VZlsGaVMrCkmm0Zd2wVsGa/FeVhdpqaE2tYdVqvsJqDZmC0mq0S3Cae6Md0GYRP/m8xWe/M0rRf6UgV1qRu7Vqtaa3RjuwdaXBtIp0DVgLuq0V1K1m7+qWnKMVFJWGWitWmLzLFTSVgqIyYKyGvNGwJPdr8r9WsK4VLStFe1BUrK0VlLWCsmYH60qxAmRb1Jq8IQMrugdlDWTtujWmhaLWsG41LFvNii+gNmCsJv2tFSxrzSu0gly1YJSClVbsB+lpK81iUS0YpXgntYJVrdkbVVtUGla15r1Sa1BtwSpFPitYKg25VuzOolbwTLE/a82LfKW0Nc6wm8tKE45oFdlaSRZ+o1m7ldKgKw1lreHKKPJSawWFhjurybCCwqxqTQ6mGxaLgJSaXF5rUFqz9Aut2GutWZwrWvKZjSIrLcRaoQW1gqpWZKRlrXgJraCuFBjLzq0qzcYta+YeWTvN2tXsba1Y6BWvDZ8bVqXiQ9rSb1ozF+WaXFtWmo0PrqzhZeWqYW9VpUGxR/JKaXY0qxW52Gpot0qzcspKk08qzQazqjUvYMW+VJp9yBW7v1ZgjIan/wXt5wSh

Steps to reproduce

  1. Create a component with <Teleport to="body">
  2. Add a reactive ref: const highlightedId = ref(null)
  3. Create a computed property that depends on this ref
  4. Display the computed value in the teleported template
  5. Add a button that changes the ref value
  6. Click the button and observe:
    • ✅ Console shows ref changed
    • ✅ Computed property runs
    • Template does NOT update

What is expected?

When a reactive ref changes inside a component using <Teleport>:

  • ✅ Computed properties should recalculate (WORKS)
  • ✅ Watchers should trigger (WORKS)
  • Template should re-render with new data (EXPECTED)

What is actually happening?

  • ✅ Reactive refs update correctly
  • ✅ Computed properties run and return new values
  • ✅ Watchers fire with correct new/old values
  • Template NEVER re-renders - DOM stays frozen with old data

This creates a complete disconnect where JavaScript reactivity works perfectly but the visual layer is completely broken.

Minimal Reproduction

Component Code

<template>
  <Teleport to="body">
    <div class="modal">
      <!-- This text NEVER updates when highlightedId changes -->
      <div v-if="highlightedItem">
        Selected: {{ highlightedItem.name }}
      </div>
      
      <!-- This style NEVER changes -->
      <div :style="{ background: highlightedId ? 'red' : 'blue' }">
        Status indicator
      </div>
      
      <!-- Button to change the ref -->
      <button @click="selectItem('item-123')">
        Select Item
      </button>
    </div>
  </Teleport>
</template>

<script setup>
import { ref, computed, watch } from 'vue'

const items = ref([
  { id: 'item-123', name: 'Item 123' },
  { id: 'item-456', name: 'Item 456' }
])

const highlightedId = ref(null)

// ✅ This computed DOES run when highlightedId changes
const highlightedItem = computed(() => {
  console.log('Computing for ID:', highlightedId.value) // LOGS CORRECTLY
  return items.value.find(i => i.id === highlightedId.value)
})

// ✅ This watcher DOES fire when highlightedId changes
watch(highlightedId, (newId) => {
  console.log('ID changed to:', newId) // LOGS CORRECTLY
})

// This function DOES change the ref value
const selectItem = (id) => {
  highlightedId.value = id
  console.log('Set ID to:', id) // LOGS CORRECTLY
  // But template shows OLD value! ❌
}
</script>

Console Output (Everything Works)

Set ID to: item-123
Computing for ID: item-123  
ID changed to: item-123

DOM Output (Nothing Updates)

<!-- Still shows nothing or old value -->
<div class="modal">
  <!-- This stays empty even though highlightedItem is now defined -->
  
  <!-- This stays blue even though highlightedId is now truthy -->
  <div style="background: blue">
    Status indicator
  </div>
</div>

System Info

OS: Linux
Node: v20.19.0
Package Manager: npm
Build Tool: Vite 7.0.6
Vue: 3.5.18
Component Type: Modal with <Teleport to="body">

Any additional comments?

Debugging attempts performed (none of these worked):

1. Using nextTick()

const selectItem = async (id) => {
  highlightedId.value = id
  await nextTick()
  // Template still frozen ❌
}

2. Using v-show instead of v-if

<div v-show="highlightedItem">  <!-- Still frozen ❌ -->

3. Adding :key to child elements

<div :key="highlightedId">  <!-- Still frozen ❌ -->

4. Using reactive() instead of ref()

const state = reactive({ highlightedId: null })  // Still frozen ❌

5. Force update methods

instance.proxy.$forceUpdate()  // No effect ❌

The ONLY Working Workaround:

Adding a "render key" to the root element inside <Teleport> and incrementing it on every change:

<template>
  <Teleport to="body">
    <!-- KEY on root element forces full re-render -->
    <div class="modal" :key="`modal-${renderKey}`">
      <div v-if="highlightedItem">
        Selected: {{ highlightedItem.name }}  <!-- NOW UPDATES ✅ -->
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { ref, computed } from 'vue'

const highlightedId = ref(null)
const renderKey = ref(0)  // Force re-render counter

const highlightedItem = computed(() => {
  return items.value.find(i => i.id === highlightedId.value)
})

const selectItem = (id) => {
  highlightedId.value = id
  renderKey.value++  // 🔑 Force entire component to re-create

Why this "works" (but is a terrible workaround):

Changing the :key forces Vue to destroy and recreate the entire DOM subtree, bypassing the Teleport reactivity bug. However, this is:

  • ❌ Expensive (full component remount)
  • ❌ Loses component state
  • ❌ Runs all lifecycle hooks again
  • ❌ Not a proper solution, just a workaround

Any additional comments?

This bug is extremely confusing for developers because:

  1. No errors or warnings - Vue is completely silent
  2. Reactivity appears to work - console logs show correct values
  3. Looks like user error - easy to assume you did something wrong
  4. Hard to diagnose - takes extensive debugging to realize it's a framework issue

Our debugging experience:

  • Time to diagnose: ~2 hours
  • Iterations attempted: ~20 different approaches
  • Where we got stuck: Computed properties and watchers working perfectly made us think our code was correct
  • What revealed it: Comparing console.log output to DOM inspector showed complete mismatch

Root cause hypothesis:

The <Teleport> component appears to disconnect teleported DOM from Vue's reactivity tracking:

  1. Teleport moves content to <body> (or other target)
  2. Vue's reactivity system continues tracking dependencies (refs, computeds work)
  3. But the render/patch mechanism loses the connection to the teleported subtree
  4. When dependencies update, the render function doesn't get called for teleported content
  5. Only full component recreation (via :key change) forces a render

Expected behavior:

<Teleport> should maintain full reactivity regardless of where content is moved in the DOM. Developers should not need workarounds like the renderKey pattern.

Our real-world use case:

We encountered this in a modal component showing financial trading data:

  • Modal uses <Teleport to="body"> for proper z-index stacking
  • Users click table rows to highlight chart positions
  • Chart should update to show highlighted trade
  • Without workaround: chart stays completely frozen despite all data updating

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions