Skip to content

Commit 5efb8bd

Browse files
committed
fix(runtime-core): defer teleport patches until suspense resolves
1 parent dc6c416 commit 5efb8bd

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2197,6 +2197,92 @@ describe('Suspense', () => {
21972197
expect(serializeInner(target)).toBe(`<div>teleported</div>`)
21982198
})
21992199

2200+
test('should patch teleport before suspense is resolved', async () => {
2201+
const target = nodeOps.createElement('div')
2202+
const text = ref('one')
2203+
2204+
const Async = defineAsyncComponent({
2205+
render() {
2206+
return h('div', 'async')
2207+
},
2208+
})
2209+
2210+
const Comp = {
2211+
setup() {
2212+
return () =>
2213+
h(Suspense, null, {
2214+
default: h('div', null, [
2215+
h(Async),
2216+
h(Teleport, { to: target }, h('div', text.value)),
2217+
]),
2218+
fallback: h('div', 'fallback'),
2219+
})
2220+
},
2221+
}
2222+
2223+
const root = nodeOps.createElement('div')
2224+
render(h(Comp), root)
2225+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2226+
expect(serializeInner(target)).toBe(``)
2227+
2228+
text.value = 'two'
2229+
await nextTick()
2230+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2231+
expect(serializeInner(target)).toBe(``)
2232+
2233+
await Promise.all(deps)
2234+
await nextTick()
2235+
expect(serializeInner(root)).toBe(
2236+
`<div><div>async</div><!--teleport start--><!--teleport end--></div>`,
2237+
)
2238+
expect(serializeInner(target)).toBe(`<div>two</div>`)
2239+
})
2240+
2241+
test('should handle disabled teleport updates before suspense is resolved', async () => {
2242+
const target = nodeOps.createElement('div')
2243+
const disabled = ref(false)
2244+
2245+
const Async = defineAsyncComponent({
2246+
render() {
2247+
return h('div', 'async')
2248+
},
2249+
})
2250+
2251+
const Comp = {
2252+
setup() {
2253+
return () =>
2254+
h(Suspense, null, {
2255+
default: h('div', null, [
2256+
h(Async),
2257+
h(
2258+
Teleport,
2259+
{ to: target, disabled: disabled.value },
2260+
h('div', 'teleported'),
2261+
),
2262+
]),
2263+
fallback: h('div', 'fallback'),
2264+
})
2265+
},
2266+
}
2267+
2268+
const root = nodeOps.createElement('div')
2269+
render(h(Comp), root)
2270+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2271+
expect(serializeInner(target)).toBe(``)
2272+
2273+
disabled.value = true
2274+
await nextTick()
2275+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2276+
expect(serializeInner(target)).toBe(``)
2277+
2278+
await Promise.all(deps)
2279+
await nextTick()
2280+
expect(serializeInner(root)).toBe(
2281+
`<div><div>async</div><!--teleport start--><div>teleported</div><!--teleport end--></div>`,
2282+
)
2283+
expect(serializeInner(target)).toBe(``)
2284+
})
2285+
22002286
describe('warnings', () => {
22012287
// base function to check if a combination of slots warns or not
22022288
function baseCheckWarn(

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,29 @@ export const TeleportImpl = {
176176
const target = (n2.target = n1.target)!
177177
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
178178
const wasDisabled = isTeleportDisabled(n1.props)
179+
180+
// When teleport target mounting is deferred into a pending parent suspense's
181+
// effect queue, patching before the suspense resolves would run against an
182+
// uninitialized target. Defer the teleport patch itself so it runs after
183+
// the suspense flushes its effects.
184+
if (parentSuspense && parentSuspense.pendingBranch && !targetAnchor) {
185+
queuePostRenderEffect(() => {
186+
TeleportImpl.process(
187+
n1,
188+
n2,
189+
container,
190+
anchor,
191+
parentComponent,
192+
parentSuspense,
193+
namespace,
194+
slotScopeIds,
195+
optimized,
196+
internals,
197+
)
198+
}, parentSuspense)
199+
return
200+
}
201+
179202
const currentContainer = wasDisabled ? container : target
180203
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
181204

0 commit comments

Comments
 (0)