Introducing Teleport (aka Portal)


This article is available as a screencast!

Let’s take a look at the new <Teleport> feature, recently renamed from <Portal>, in Vue.js 3, and some of the interesting things you might do with it.

To get a basic of idea what <Teleport> does, read the RFC. Basically, it allows you to render a component at an alternative location on the page by wrapping it in <Teleport>.

A first look with render functions

The first interesting thing I noticed was when writing a render function in TypeScript. For example:

import { createApp, h, defineComponent, Teleport } from 'vue'

const App = defineComponent({
  render() {
    return h(() => [
      h('div', { id: 'dest' }),
      h('div', 'Hello world'),
      h(Teleport, { to: '#dest' })
    ])
  }
})

createApp(App).mount('#app')

The idea here is the final h will be teleported to the first h, specified by the to prop. This will actually give us a compile time error - <Teleport> needs some content to, well, teleport, and we haven’t given it any. This is the first time I’ve actually seen a component give a compile time error when the third argument to h, the children to render, was not provided. We can make this into a working example by adding children:

h(Teleport, { to: '#dest' }, 'This will be teleported')

Another caveat, that may well be a bug, is that <Teleport> and the destination must be in components without a root node - also known as a fragment. That is why I am using the h() => [...]) syntax. I am guessing this is a bug that will be fixed in a future alpha (at the time of this article, the latest version of Vue 3 is alpha-12.

Binding to Teleport

Let’s move away from the world of render functions, back into good old .vue files. We are going to build a small app that let’s us teleport some elements around using Vue’s reactivity. Start with the following:

<template>
  <label for="top">Top</label>
  <input id="top" type="radio" value="top" v-model="selected" />

  <label for="middle">Middle</label>
  <input id="middle" type="radio" value="middle" v-model="selected" />

  <label for="bottom">Bottom</label>
  <input id="bottom" type="radio" value="bottom"  v-model="selected" />
  <br />
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const selected = ref<'top' | 'middle' | 'bottom'>('top')

    return {
      selected,
    }
  }
})
</script>

So far this doesn’t do much - we have three radio buttons bound to the selected variable. Notice there is no root node - this is the bug I was mentioning earlier.

Next, we will add three destinations - yep, a <Teleport> doesn’t have to be locked to a specific destination:

<template>
  <!-- ... -->
  <h1>Top</h1>
  <div id="top-teleport"></div>
  <h1>Middle</h1>
  <div id="middle-teleport"></div>
  <h1>Bottom</h1>
  <div id="bottom-teleport"></div>
</template>

You might see what’s coming next - now we have 3 radio buttons and 3 destinations, let’s finally add in the <Teleport>:

<template>
  <!-- ... -->
  <teleport :to="destination">
    This will teleport
  </teleport>
</template>

We are binding to a destination variable. Let’s create that and make sure it updates when the selected does using a computed property:

<script lang="ts">
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  setup() {
    const selected = ref<'top' | 'middle' | 'bottom'>('top')

    const destination = computed(() => `#${selected.value}-teleport`)

    return {
      destination,
      selected,
    }
  }
})
</script>

This is enough to get everything working! It’s hard to appreciate what’s going on - best watch the accompanying sreencast to really get an idea, but as you change the selected <Teleport> using the radio buttons, the content is reactively teleported!

The main use case for this is probably a modal, or something that needs to be rendered at the top level of the application, but is triggered from somewhere else.

Disabling a <Teleport>

It is also possible to disable a <Teleport>. A good use case for this would be when you want to close a modal, potentially. Let’s add a checkbox to support this:

<template>
  <!-- ... -->
  <label for="top-disabled">Disabled</label>
  <input id="top-disabled" type="checkbox" v-model="disabled" />

  <teleport :to="destination" :disabled="disabled">
  </teleport>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue'

export default defineComponent({
  setup() {
    //...
    const disabled = ref(false)

    return {
      disabled,
      destination,
      selected,
    }
  }
})
</script>

Now if you try disabling the <Teleport>, you will notice the original content is rendered back where it started - this this case, at the bottom of the page.

<Teleport> maintains the state of the DOM

When content is moved around, it preserves the state of the DOM. A good example of this is using a <video> element. If you move a <video> that is playing using a <Teleport>, the video will keep on playing! <Teleport> isn’t rerendering the DOM element - it’s really moving the actual element as is, without breaking the state or rerendering it. Very cool.

Conclusion

We learned some of the neat features of <Teleport>, how to bind to it’s to prop, about it’s disabled prop, and how it preseves the state of the DOM elements it is moving.


Register your email to get occasional emails about new screencasts and courses!
Thanks for registering!