Skip to content

Commit 88ed045

Browse files
authored
fix(runtime-dom): defer teleport mount/update until suspense resolves (#8619)
close #8603
1 parent 908c6ad commit 88ed045

2 files changed

Lines changed: 138 additions & 8 deletions

File tree

packages/runtime-core/__tests__/components/Suspense.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
KeepAlive,
99
Suspense,
1010
type SuspenseProps,
11+
Teleport,
1112
createCommentVNode,
1213
h,
1314
nextTick,
@@ -2165,6 +2166,127 @@ describe('Suspense', () => {
21652166
await Promise.all(deps)
21662167
})
21672168

2169+
test('should mount after suspense is resolved', async () => {
2170+
const target = nodeOps.createElement('div')
2171+
2172+
const Async = defineAsyncComponent({
2173+
render() {
2174+
return h('div', 'async')
2175+
},
2176+
})
2177+
2178+
const Comp = {
2179+
setup() {
2180+
return () =>
2181+
h(Suspense, null, {
2182+
default: h('div', null, [
2183+
h(Async),
2184+
h(Teleport, { to: target }, h('div', 'teleported')),
2185+
]),
2186+
fallback: h('div', 'fallback'),
2187+
})
2188+
},
2189+
}
2190+
2191+
const root = nodeOps.createElement('div')
2192+
render(h(Comp), root)
2193+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2194+
expect(serializeInner(target)).toBe(``)
2195+
2196+
await Promise.all(deps)
2197+
await nextTick()
2198+
expect(serializeInner(root)).toBe(
2199+
`<div><div>async</div><!--teleport start--><!--teleport end--></div>`,
2200+
)
2201+
expect(serializeInner(target)).toBe(`<div>teleported</div>`)
2202+
})
2203+
2204+
test('should patch teleport before suspense is resolved', async () => {
2205+
const target = nodeOps.createElement('div')
2206+
const text = ref('one')
2207+
2208+
const Async = defineAsyncComponent({
2209+
render() {
2210+
return h('div', 'async')
2211+
},
2212+
})
2213+
2214+
const Comp = {
2215+
setup() {
2216+
return () =>
2217+
h(Suspense, null, {
2218+
default: h('div', null, [
2219+
h(Async),
2220+
h(Teleport, { to: target }, h('div', text.value)),
2221+
]),
2222+
fallback: h('div', 'fallback'),
2223+
})
2224+
},
2225+
}
2226+
2227+
const root = nodeOps.createElement('div')
2228+
render(h(Comp), root)
2229+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2230+
expect(serializeInner(target)).toBe(``)
2231+
2232+
text.value = 'two'
2233+
await nextTick()
2234+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2235+
expect(serializeInner(target)).toBe(``)
2236+
2237+
await Promise.all(deps)
2238+
await nextTick()
2239+
expect(serializeInner(root)).toBe(
2240+
`<div><div>async</div><!--teleport start--><!--teleport end--></div>`,
2241+
)
2242+
expect(serializeInner(target)).toBe(`<div>two</div>`)
2243+
})
2244+
2245+
test('should handle disabled teleport updates before suspense is resolved', async () => {
2246+
const target = nodeOps.createElement('div')
2247+
const disabled = ref(false)
2248+
2249+
const Async = defineAsyncComponent({
2250+
render() {
2251+
return h('div', 'async')
2252+
},
2253+
})
2254+
2255+
const Comp = {
2256+
setup() {
2257+
return () =>
2258+
h(Suspense, null, {
2259+
default: h('div', null, [
2260+
h(Async),
2261+
h(
2262+
Teleport,
2263+
{ to: target, disabled: disabled.value },
2264+
h('div', 'teleported'),
2265+
),
2266+
]),
2267+
fallback: h('div', 'fallback'),
2268+
})
2269+
},
2270+
}
2271+
2272+
const root = nodeOps.createElement('div')
2273+
render(h(Comp), root)
2274+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2275+
expect(serializeInner(target)).toBe(``)
2276+
2277+
disabled.value = true
2278+
await nextTick()
2279+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2280+
expect(serializeInner(target)).toBe(``)
2281+
2282+
await Promise.all(deps)
2283+
await nextTick()
2284+
expect(serializeInner(root)).toBe(
2285+
`<div><div>async</div><!--teleport start--><div>teleported</div><!--teleport end--></div>`,
2286+
)
2287+
expect(serializeInner(target)).toBe(``)
2288+
})
2289+
21682290
//#11617
21692291
test('update async component before resolve then update again', async () => {
21702292
const arr: boolean[] = []

packages/runtime-core/src/components/Teleport.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,31 @@ export const TeleportImpl = {
169169
updateCssVars(n2, true)
170170
}
171171

172-
if (isTeleportDeferred(n2.props)) {
172+
if (
173+
isTeleportDeferred(n2.props) ||
174+
(parentSuspense && parentSuspense.pendingBranch)
175+
) {
173176
n2.el!.__isMounted = false
174177
queuePostRenderEffect(() => {
178+
if (n2.el!.__isMounted !== false) return
175179
mountToTarget()
176180
delete n2.el!.__isMounted
177181
}, parentSuspense)
178182
} else {
179183
mountToTarget()
180184
}
181185
} else {
182-
if (isTeleportDeferred(n2.props) && n1.el!.__isMounted === false) {
186+
// update content
187+
n2.el = n1.el
188+
n2.targetStart = n1.targetStart
189+
const mainAnchor = (n2.anchor = n1.anchor)!
190+
const target = (n2.target = n1.target)!
191+
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
192+
193+
// Target mounting may still be pending because of deferred teleport or a
194+
// parent suspense buffering post-render effects. In that case, defer the
195+
// teleport patch itself until the pending mount effect has run.
196+
if (n1.el!.__isMounted === false) {
183197
queuePostRenderEffect(() => {
184198
TeleportImpl.process(
185199
n1,
@@ -196,12 +210,6 @@ export const TeleportImpl = {
196210
}, parentSuspense)
197211
return
198212
}
199-
// update content
200-
n2.el = n1.el
201-
n2.targetStart = n1.targetStart
202-
const mainAnchor = (n2.anchor = n1.anchor)!
203-
const target = (n2.target = n1.target)!
204-
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
205213
const wasDisabled = isTeleportDisabled(n1.props)
206214
const currentContainer = wasDisabled ? container : target
207215
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor

0 commit comments

Comments
 (0)