]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(hydration): hydrate VaporTeleport (#14002)
authoredison <daiwei521@126.com>
Tue, 21 Oct 2025 01:10:26 +0000 (09:10 +0800)
committerGitHub <noreply@github.com>
Tue, 21 Oct 2025 01:10:26 +0000 (09:10 +0800)
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/Teleport.ts

index 346d2f813eb6d396be2f3ab39b0c8d6b31de82d6..0dc70bcd83774b963d716aba5ed8180314aacfdc 100644 (file)
@@ -394,7 +394,7 @@ function moveTeleport(
   }
 }
 
-interface TeleportTargetElement extends Element {
+export interface TeleportTargetElement extends Element {
   // last teleport target
   _lpa?: Node | null
 }
index e4803b7cc3c56da26de89895aa19ca3dab9b28d6..0565f4fbd35979df3b980c4fd0bd4232ecbed5a7 100644 (file)
@@ -576,6 +576,10 @@ export {
   isTeleportDisabled,
   isTeleportDeferred,
 } from './components/Teleport'
+/**
+ * @internal
+ */
+export type { TeleportTargetElement } from './components/Teleport'
 /**
  * @internal
  */
index 50e84e4c081b3e23cae63d5a616920893605b284..2e48ae5ceee011212bfb1f4c7dabc6f1a97daf6e 100644 (file)
@@ -6,6 +6,7 @@ import * as runtimeDom from '@vue/runtime-dom'
 import * as VueServerRenderer from '@vue/server-renderer'
 import { isString } from '@vue/shared'
 import type { VaporComponentInstance } from '../src/component'
+import type { TeleportFragment } from '../src/components/Teleport'
 
 const formatHtml = (raw: string) => {
   return raw
@@ -3008,6 +3009,590 @@ describe('Vapor Mode hydration', () => {
     })
   })
 
+  describe('teleport', () => {
+    test('basic', async () => {
+      const data = ref({
+        msg: ref('foo'),
+        disabled: ref(false),
+        fn: vi.fn(),
+      })
+
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport'
+      teleportContainer.innerHTML =
+        `<!--teleport start anchor-->` +
+        `<span>foo</span>` +
+        `<span class="foo"></span>` +
+        `<!--teleport anchor-->`
+      document.body.appendChild(teleportContainer)
+
+      const { block, container } = await mountWithHydration(
+        '<!--teleport start--><!--teleport end-->',
+        `<teleport to="#teleport" :disabled="data.disabled">
+          <span>{{data.msg}}</span>
+          <span :class="data.msg" @click="data.fn"></span>
+        </teleport>`,
+        data,
+      )
+
+      const teleport = block as TeleportFragment
+      expect(teleport.anchor).toBe(container.lastChild)
+      expect(teleport.target).toBe(teleportContainer)
+      expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+      expect((teleport.nodes as Node[])[0]).toBe(
+        teleportContainer.childNodes[1],
+      )
+      expect((teleport.nodes as Node[])[1]).toBe(
+        teleportContainer.childNodes[2],
+      )
+      expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end-->"`,
+      )
+
+      // event handler
+      triggerEvent('click', teleportContainer.querySelector('.foo')!)
+      expect(data.value.fn).toHaveBeenCalled()
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(teleportContainer.innerHTML)).toBe(
+        `<!--teleport start anchor-->` +
+          `<span>bar</span>` +
+          `<span class="bar"></span>` +
+          `<!--teleport anchor-->`,
+      )
+
+      data.value.disabled = true
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--teleport start-->` +
+          `<span>bar</span>` +
+          `<span class="bar"></span>` +
+          `<!--teleport end-->`,
+      )
+      expect(formatHtml(teleportContainer.innerHTML)).toMatchInlineSnapshot(
+        `"<!--teleport start anchor--><!--teleport anchor-->"`,
+      )
+
+      data.value.msg = 'baz'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--teleport start-->` +
+          `<span>baz</span>` +
+          `<span class="baz"></span>` +
+          `<!--teleport end-->`,
+      )
+
+      data.value.disabled = false
+      await nextTick()
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<!--teleport start--><!--teleport end-->"`,
+      )
+      expect(formatHtml(teleportContainer.innerHTML)).toBe(
+        `<!--teleport start anchor-->` +
+          `<span>baz</span>` +
+          `<span class="baz"></span>` +
+          `<!--teleport anchor-->`,
+      )
+    })
+
+    test('multiple + integration', async () => {
+      const data = ref({
+        msg: ref('foo'),
+        fn1: vi.fn(),
+        fn2: vi.fn(),
+      })
+
+      const code = `
+          <teleport to="#teleport2">
+            <span>{{data.msg}}</span>
+            <span :class="data.msg" @click="data.fn1"></span>
+          </teleport>
+          <teleport to="#teleport2">
+            <span>{{data.msg}}2</span>
+            <span :class="data.msg + 2" @click="data.fn2"></span>
+          </teleport>`
+
+      const SSRComp = compileVaporComponent(code, data, undefined, true)
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport2'
+      const ctx = {} as any
+      const mainHtml = await VueServerRenderer.renderToString(
+        runtimeDom.createSSRApp(SSRComp),
+        ctx,
+      )
+      expect(mainHtml).toBe(
+        `<!--[-->` +
+          `<!--teleport start--><!--teleport end-->` +
+          `<!--teleport start--><!--teleport end-->` +
+          `<!--]-->`,
+      )
+
+      const teleportHtml = ctx.teleports!['#teleport2']
+      expect(teleportHtml).toBe(
+        `<!--teleport start anchor-->` +
+          `<span>foo</span><span class="foo"></span>` +
+          `<!--teleport anchor-->` +
+          `<!--teleport start anchor-->` +
+          `<span>foo2</span><span class="foo2"></span>` +
+          `<!--teleport anchor-->`,
+      )
+
+      teleportContainer.innerHTML = teleportHtml
+      document.body.appendChild(teleportContainer)
+
+      const { block, container } = await mountWithHydration(
+        mainHtml,
+        code,
+        data,
+      )
+
+      const teleports = block as any as TeleportFragment[]
+      const teleport1 = teleports[0]
+      const teleport2 = teleports[1]
+      expect(teleport1.anchor).toBe(container.childNodes[2])
+      expect(teleport2.anchor).toBe(container.childNodes[4])
+
+      expect(teleport1.target).toBe(teleportContainer)
+      expect(teleport1.targetStart).toBe(teleportContainer.childNodes[0])
+      expect((teleport1.nodes as Node[])[0]).toBe(
+        teleportContainer.childNodes[1],
+      )
+      expect(teleport1.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+      expect(teleport2.target).toBe(teleportContainer)
+      expect(teleport2.targetStart).toBe(teleportContainer.childNodes[4])
+      expect((teleport2.nodes as Node[])[0]).toBe(
+        teleportContainer.childNodes[5],
+      )
+      expect(teleport2.targetAnchor).toBe(teleportContainer.childNodes[7])
+
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--teleport start--><!--teleport end-->` +
+          `<!--teleport start--><!--teleport end-->` +
+          `<!--]-->`,
+      )
+
+      // event handler
+      triggerEvent('click', teleportContainer.querySelector('.foo')!)
+      expect(data.value.fn1).toHaveBeenCalled()
+
+      triggerEvent('click', teleportContainer.querySelector('.foo2')!)
+      expect(data.value.fn2).toHaveBeenCalled()
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(teleportContainer.innerHTML).toBe(
+        `<!--teleport start anchor-->` +
+          `<span>bar</span>` +
+          `<span class="bar"></span>` +
+          `<!--teleport anchor-->` +
+          `<!--teleport start anchor-->` +
+          `<span>bar2</span>` +
+          `<span class="bar2"></span>` +
+          `<!--teleport anchor-->`,
+      )
+    })
+
+    test('disabled', async () => {
+      const data = ref({
+        msg: ref('foo'),
+        fn1: vi.fn(),
+        fn2: vi.fn(),
+      })
+
+      const code = `
+          <div>foo</div>
+          <teleport to="#teleport3" disabled="true">
+            <span>{{data.msg}}</span>
+            <span :class="data.msg" @click="data.fn1"></span>
+          </teleport>
+          <div :class="data.msg + 2" @click="data.fn2">bar</div>
+          `
+
+      const SSRComp = compileVaporComponent(code, data, undefined, true)
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport3'
+      const ctx = {} as any
+      const mainHtml = await VueServerRenderer.renderToString(
+        runtimeDom.createSSRApp(SSRComp),
+        ctx,
+      )
+      expect(mainHtml).toBe(
+        `<!--[-->` +
+          `<div>foo</div>` +
+          `<!--teleport start-->` +
+          `<span>foo</span>` +
+          `<span class="foo"></span>` +
+          `<!--teleport end-->` +
+          `<div class="foo2">bar</div>` +
+          `<!--]-->`,
+      )
+
+      const teleportHtml = ctx.teleports!['#teleport3']
+      expect(teleportHtml).toMatchInlineSnapshot(
+        `"<!--teleport start anchor--><!--teleport anchor-->"`,
+      )
+
+      teleportContainer.innerHTML = teleportHtml
+      document.body.appendChild(teleportContainer)
+
+      const { block, container } = await mountWithHydration(
+        mainHtml,
+        code,
+        data,
+      )
+
+      const blocks = block as any[]
+      expect(blocks[0]).toBe(container.childNodes[1])
+
+      const teleport = blocks[1] as TeleportFragment
+      expect((teleport.nodes as Node[])[0]).toBe(container.childNodes[3])
+      expect((teleport.nodes as Node[])[1]).toBe(container.childNodes[4])
+      expect(teleport.anchor).toBe(container.childNodes[5])
+      expect(teleport.target).toBe(teleportContainer)
+      expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+      expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[1])
+      expect(blocks[2]).toBe(container.childNodes[6])
+
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div>foo</div>` +
+          `<!--teleport start-->` +
+          `<span>foo</span>` +
+          `<span class="foo"></span>` +
+          `<!--teleport end-->` +
+          `<div class="foo2">bar</div>` +
+          `<!--]-->`,
+      )
+
+      // event handler
+      triggerEvent('click', container.querySelector('.foo')!)
+      expect(data.value.fn1).toHaveBeenCalled()
+
+      triggerEvent('click', container.querySelector('.foo2')!)
+      expect(data.value.fn2).toHaveBeenCalled()
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div>foo</div>` +
+          `<!--teleport start-->` +
+          `<span>bar</span>` +
+          `<span class="bar"></span>` +
+          `<!--teleport end-->` +
+          `<div class="bar2">bar</div>` +
+          `<!--]-->`,
+      )
+    })
+
+    test('disabled + as component root', async () => {
+      const { container } = await mountWithHydration(
+        `<!--[-->` +
+          `<div>Parent fragment</div>` +
+          `<!--teleport start--><div>Teleport content</div><!--teleport end-->` +
+          `<!--]-->`,
+        `
+          <div>Parent fragment</div>
+          <teleport to="body" disabled>
+            <div>Teleport content</div>
+          </teleport>
+        `,
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div>Parent fragment</div>` +
+          `<!--teleport start-->` +
+          `<div>Teleport content</div>` +
+          `<!--teleport end-->` +
+          `<!--]-->`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+    })
+
+    test('as component root', async () => {
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport4'
+      teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
+      document.body.appendChild(teleportContainer)
+
+      const { block, container } = await mountWithHydration(
+        '<!--teleport start--><!--teleport end-->',
+        `<components.Wrapper></components.Wrapper>`,
+        undefined,
+        {
+          Wrapper: compileVaporComponent(
+            `<teleport to="#teleport4">hello</teleport>`,
+          ),
+        },
+      )
+
+      const teleport = (block as VaporComponentInstance)
+        .block as TeleportFragment
+      expect(teleport.anchor).toBe(container.childNodes[1])
+      expect(teleport.target).toBe(teleportContainer)
+      expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+      expect(teleport.nodes).toBe(teleportContainer.childNodes[1])
+      expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[2])
+    })
+
+    test('nested', async () => {
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = 'teleport5'
+      teleportContainer.innerHTML =
+        `<!--teleport start anchor-->` +
+        `<!--teleport start--><!--teleport end-->` +
+        `<!--teleport anchor-->` +
+        `<!--teleport start anchor-->` +
+        `<div>child</div>` +
+        `<!--teleport anchor-->`
+      document.body.appendChild(teleportContainer)
+
+      const { block, container } = await mountWithHydration(
+        '<!--teleport start--><!--teleport end-->',
+        `<teleport to="#teleport5">
+          <teleport to="#teleport5"><div>child</div></teleport>
+        </teleport>`,
+      )
+
+      const teleport = block as TeleportFragment
+      expect(teleport.anchor).toBe(container.childNodes[1])
+      expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+      expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+      const childTeleport = teleport.nodes as TeleportFragment
+      expect(childTeleport.anchor).toBe(teleportContainer.childNodes[2])
+      expect(childTeleport.targetStart).toBe(teleportContainer.childNodes[4])
+      expect(childTeleport.targetAnchor).toBe(teleportContainer.childNodes[6])
+      expect(childTeleport.nodes).toBe(teleportContainer.childNodes[5])
+    })
+
+    test('unmount (full integration)', async () => {
+      const targetId = 'teleport6'
+      const data = ref({
+        toggle: ref(true),
+      })
+
+      const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
+      const Comp1 = compileVaporComponent(template1)
+      const SSRComp1 = compileVaporComponent(
+        template1,
+        undefined,
+        undefined,
+        true,
+      )
+
+      const template2 = `<div>Comp2</div>`
+      const Comp2 = compileVaporComponent(template2)
+      const SSRComp2 = compileVaporComponent(
+        template2,
+        undefined,
+        undefined,
+        true,
+      )
+
+      const appCode = `
+        <div>
+          <components.Comp1 v-if="data.toggle"/>
+          <components.Comp2 v-else/>
+        </div>
+      `
+
+      const SSRApp = compileVaporComponent(
+        appCode,
+        data,
+        {
+          Comp1: SSRComp1,
+          Comp2: SSRComp2,
+        },
+        true,
+      )
+
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = targetId
+      document.body.appendChild(teleportContainer)
+
+      const ctx = {} as any
+      const mainHtml = await VueServerRenderer.renderToString(
+        runtimeDom.createSSRApp(SSRApp),
+        ctx,
+      )
+      expect(mainHtml).toBe(
+        '<div><!--teleport start--><!--teleport end--></div>',
+      )
+      teleportContainer.innerHTML = ctx.teleports![`#${targetId}`]
+
+      const { container } = await mountWithHydration(mainHtml, appCode, data, {
+        Comp1,
+        Comp2,
+      })
+
+      expect(container.innerHTML).toBe(
+        '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      )
+      expect(teleportContainer.innerHTML).toBe(
+        `<!--teleport start anchor-->` +
+          `<span>Teleported Comp1</span>` +
+          `<!--teleport anchor-->`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+
+      data.value.toggle = false
+      await nextTick()
+      expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+      expect(teleportContainer.innerHTML).toBe('')
+    })
+
+    test('unmount (mismatch + full integration)', async () => {
+      const targetId = 'teleport7'
+      const data = ref({
+        toggle: ref(true),
+      })
+
+      const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
+      const Comp1 = compileVaporComponent(template1)
+      const SSRComp1 = compileVaporComponent(
+        template1,
+        undefined,
+        undefined,
+        true,
+      )
+
+      const template2 = `<div>Comp2</div>`
+      const Comp2 = compileVaporComponent(template2)
+      const SSRComp2 = compileVaporComponent(
+        template2,
+        undefined,
+        undefined,
+        true,
+      )
+
+      const appCode = `
+        <div>
+          <components.Comp1 v-if="data.toggle"/>
+          <components.Comp2 v-else/>
+        </div>
+      `
+
+      const SSRApp = compileVaporComponent(
+        appCode,
+        data,
+        {
+          Comp1: SSRComp1,
+          Comp2: SSRComp2,
+        },
+        true,
+      )
+
+      const teleportContainer = document.createElement('div')
+      teleportContainer.id = targetId
+      document.body.appendChild(teleportContainer)
+
+      const mainHtml = await VueServerRenderer.renderToString(
+        runtimeDom.createSSRApp(SSRApp),
+      )
+      expect(mainHtml).toBe(
+        '<div><!--teleport start--><!--teleport end--></div>',
+      )
+      expect(teleportContainer.innerHTML).toBe('')
+
+      const { container } = await mountWithHydration(mainHtml, appCode, data, {
+        Comp1,
+        Comp2,
+      })
+
+      expect(container.innerHTML).toBe(
+        '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      )
+      expect(teleportContainer.innerHTML).toBe(`<span>Teleported Comp1</span>`)
+      expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+      data.value.toggle = false
+      await nextTick()
+      expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+      expect(teleportContainer.innerHTML).toBe('')
+    })
+
+    test('target change (mismatch + full integration)', async () => {
+      const targetId1 = 'teleport8-1'
+      const targetId2 = 'teleport8-2'
+      const data = ref({
+        target: ref(targetId1),
+        msg: ref('foo'),
+      })
+
+      const template = `<Teleport :to="'#' + data.target"><span>{{data.msg}}</span></Teleport>`
+      const Comp = compileVaporComponent(template, data)
+      const SSRComp = compileVaporComponent(template, data, undefined, true)
+
+      const teleportContainer1 = document.createElement('div')
+      teleportContainer1.id = targetId1
+      const teleportContainer2 = document.createElement('div')
+      teleportContainer2.id = targetId2
+      document.body.appendChild(teleportContainer1)
+      document.body.appendChild(teleportContainer2)
+
+      // server render
+      const mainHtml = await VueServerRenderer.renderToString(
+        runtimeDom.createSSRApp(SSRComp),
+      )
+      expect(mainHtml).toBe(`<!--teleport start--><!--teleport end-->`)
+      expect(teleportContainer1.innerHTML).toBe('')
+      expect(teleportContainer2.innerHTML).toBe('')
+
+      // hydrate
+      const { container } = await mountWithHydration(mainHtml, template, data, {
+        Comp,
+      })
+
+      expect(container.innerHTML).toBe(
+        `<!--teleport start--><!--teleport end-->`,
+      )
+      expect(teleportContainer1.innerHTML).toBe(`<span>foo</span>`)
+      expect(teleportContainer2.innerHTML).toBe('')
+      expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+      data.value.target = targetId2
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--teleport start--><!--teleport end-->`,
+      )
+      expect(teleportContainer1.innerHTML).toBe('')
+      expect(teleportContainer2.innerHTML).toBe(`<span>bar</span>`)
+    })
+
+    test('with disabled teleport + undefined target', async () => {
+      const data = ref({
+        msg: ref('foo'),
+      })
+
+      const { container } = await mountWithHydration(
+        '<!--teleport start--><span>foo</span><!--teleport end-->',
+        `<teleport :to="undefined" :disabled="true">
+          <span>{{data.msg}}</span>
+        </teleport>`,
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<!--teleport start--><span>foo</span><!--teleport end-->`,
+      )
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--teleport start--><span>bar</span><!--teleport end-->`,
+      )
+    })
+  })
+
+  describe.todo('Suspense')
+
   describe('force hydrate prop', async () => {
     test('force hydrate prop with `.prop` modifier', async () => {
       const { container } = await mountWithHydration(
@@ -3053,8 +3638,6 @@ describe('Vapor Mode hydration', () => {
     // vapor custom element not implemented yet
     test.todo('force hydrate custom element with dynamic props', () => {})
   })
-
-  describe.todo('Suspense')
 })
 
 describe('mismatch handling', () => {
index e1e4cffe16e0e244fcf664132da5968ec4ecbe7e..6ea662583dbb87fda341068e4994b9ab40d4e77d 100644 (file)
@@ -224,10 +224,13 @@ export function createComponent(
   // teleport
   if (isVaporTeleport(component)) {
     const frag = component.process(rawProps!, rawSlots!)
-    if (!isHydrating && _insertionParent) {
-      insert(frag, _insertionParent, _insertionAnchor)
+    if (!isHydrating) {
+      if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
     } else {
       frag.hydrate()
+      if (_isLastInsertion) {
+        advanceHydrationNode(_insertionParent!)
+      }
     }
 
     return frag as any
index 8609303df23f16e5c9a8a8bab5225db2421f7fb1..ef3d4598c9be7f956b11fc3094fbb716f14e11c6 100644 (file)
@@ -1,5 +1,8 @@
 import {
+  MismatchTypes,
   type TeleportProps,
+  type TeleportTargetElement,
+  isMismatchAllowed,
   isTeleportDeferred,
   isTeleportDisabled,
   queuePostFlushCb,
@@ -17,6 +20,15 @@ import { rawPropsProxyHandlers } from '../componentProps'
 import { renderEffect } from '../renderEffect'
 import { extend, isArray } from '@vue/shared'
 import { VaporFragment } from '../fragment'
+import {
+  advanceHydrationNode,
+  currentHydrationNode,
+  isComment,
+  isHydrating,
+  logMismatchError,
+  runWithoutHydration,
+  setCurrentHydrationNode,
+} from '../dom/hydration'
 
 export const VaporTeleportImpl = {
   name: 'VaporTeleport',
@@ -46,7 +58,11 @@ export class TeleportFragment extends VaporFragment {
     super([])
     this.rawProps = props
     this.rawSlots = slots
-    this.anchor = __DEV__ ? createComment('teleport end') : createTextNode()
+    this.anchor = isHydrating
+      ? undefined
+      : __DEV__
+        ? createComment('teleport end')
+        : createTextNode()
 
     renderEffect(() => {
       // access the props to trigger tracking
@@ -60,7 +76,9 @@ export class TeleportFragment extends VaporFragment {
       this.handlePropsUpdate()
     })
 
-    this.initChildren()
+    if (!isHydrating) {
+      this.initChildren()
+    }
   }
 
   get parent(): ParentNode | null {
@@ -74,7 +92,6 @@ export class TeleportFragment extends VaporFragment {
       )
     })
 
-    // for hmr
     if (__DEV__) {
       const nodes = this.nodes
       if (isVaporComponent(nodes)) {
@@ -89,7 +106,7 @@ export class TeleportFragment extends VaporFragment {
 
   private handleChildrenUpdate(children: Block): void {
     // not mounted yet
-    if (!this.parent) {
+    if (!this.parent || isHydrating) {
       this.nodes = children
       return
     }
@@ -102,7 +119,7 @@ export class TeleportFragment extends VaporFragment {
 
   private handlePropsUpdate(): void {
     // not mounted yet
-    if (!this.parent) return
+    if (!this.parent || isHydrating) return
 
     const mount = (parent: ParentNode, anchor: Node | null) => {
       insert(
@@ -153,6 +170,8 @@ export class TeleportFragment extends VaporFragment {
   }
 
   insert = (container: ParentNode, anchor: Node | null): void => {
+    if (isHydrating) return
+
     // insert anchors in the main view
     this.placeholder = __DEV__
       ? createComment('teleport start')
@@ -191,8 +210,85 @@ export class TeleportFragment extends VaporFragment {
     this.mountAnchor = undefined
   }
 
+  private hydrateDisabledTeleport(targetNode: Node | null): void {
+    let nextNode = this.placeholder!.nextSibling!
+    setCurrentHydrationNode(nextNode)
+    this.mountAnchor = this.anchor = locateTeleportEndAnchor(nextNode)!
+    this.mountContainer = this.anchor.parentNode
+    this.targetStart = targetNode
+    this.targetAnchor = targetNode && targetNode.nextSibling
+    this.initChildren()
+  }
+
+  private mount(target: Node): void {
+    target.appendChild((this.targetStart = createTextNode('')))
+    target.appendChild(
+      (this.mountAnchor = this.targetAnchor = createTextNode('')),
+    )
+
+    if (!isMismatchAllowed(target as Element, MismatchTypes.CHILDREN)) {
+      if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
+        warn(
+          `Hydration children mismatch on`,
+          target,
+          `\nServer rendered element contains fewer child nodes than client nodes.`,
+        )
+      }
+      logMismatchError()
+    }
+
+    runWithoutHydration(this.initChildren.bind(this))
+  }
+
   hydrate = (): void => {
-    //TODO
+    const target = (this.target = resolveTeleportTarget(
+      this.resolvedProps!,
+      querySelector,
+    ))
+    const disabled = isTeleportDisabled(this.resolvedProps!)
+    this.placeholder = currentHydrationNode!
+    if (target) {
+      const targetNode =
+        (target as TeleportTargetElement)._lpa || target.firstChild
+      if (disabled) {
+        this.hydrateDisabledTeleport(targetNode)
+      } else {
+        this.anchor = locateTeleportEndAnchor()!
+        this.mountContainer = target
+        let targetAnchor = targetNode
+        while (targetAnchor) {
+          if (targetAnchor && targetAnchor.nodeType === 8) {
+            if ((targetAnchor as Comment).data === 'teleport start anchor') {
+              this.targetStart = targetAnchor
+            } else if ((targetAnchor as Comment).data === 'teleport anchor') {
+              this.mountAnchor = this.targetAnchor = targetAnchor
+              ;(target as TeleportTargetElement)._lpa =
+                this.targetAnchor && this.targetAnchor.nextSibling
+              break
+            }
+          }
+          targetAnchor = targetAnchor.nextSibling
+        }
+
+        if (targetNode) {
+          setCurrentHydrationNode(targetNode.nextSibling)
+        }
+
+        // if the HTML corresponding to Teleport is not embedded in the
+        // correct position on the final page during SSR. the targetAnchor will
+        // always be null, we need to manually add targetAnchor to ensure
+        // Teleport it can properly unmount or move
+        if (!this.targetAnchor) {
+          this.mount(target)
+        } else {
+          this.initChildren()
+        }
+      }
+    } else if (disabled) {
+      this.hydrateDisabledTeleport(currentHydrationNode!)
+    }
+
+    advanceHydrationNode(this.anchor!)
   }
 }
 
@@ -201,3 +297,15 @@ export function isVaporTeleport(
 ): value is typeof VaporTeleportImpl {
   return value === VaporTeleportImpl
 }
+
+function locateTeleportEndAnchor(
+  node: Node = currentHydrationNode!,
+): Node | null {
+  while (node) {
+    if (isComment(node, 'teleport end')) {
+      return node
+    }
+    node = node.nextSibling as Node
+  }
+  return null
+}