Skip to content

Commit a2c1700

Browse files
authored
fix(suspense): update suspense vnode's el during branch self-update (#12922)
close #12920
1 parent b39e032 commit a2c1700

File tree

2 files changed

+199
-3
lines changed

2 files changed

+199
-3
lines changed

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

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import {
99
Suspense,
1010
type SuspenseProps,
1111
Teleport,
12+
createBlock,
1213
createCommentVNode,
14+
createElementBlock,
1315
h,
1416
nextTick,
1517
nodeOps,
1618
onErrorCaptured,
1719
onMounted,
1820
onUnmounted,
1921
onUpdated,
22+
openBlock,
2023
ref,
2124
render,
2225
renderList,
@@ -28,9 +31,17 @@ import {
2831
watchEffect,
2932
withDirectives,
3033
} from '@vue/runtime-test'
31-
import { computed, createApp, defineComponent, inject, provide } from 'vue'
34+
import {
35+
computed,
36+
createApp,
37+
defineAsyncComponent as defineAsyncComp,
38+
defineComponent,
39+
inject,
40+
provide,
41+
} from 'vue'
3242
import type { RawSlots } from 'packages/runtime-core/src/componentSlots'
3343
import { resetSuspenseId } from '../../src/components/Suspense'
44+
import { PatchFlags } from '@vue/shared'
3445

3546
describe('Suspense', () => {
3647
const deps: Promise<any>[] = []
@@ -2166,6 +2177,184 @@ describe('Suspense', () => {
21662177
await Promise.all(deps)
21672178
})
21682179

2180+
// #12920
2181+
test('unmount Suspense after async child (with defineAsyncComponent) self-triggered update', async () => {
2182+
const Comp = defineComponent({
2183+
setup() {
2184+
const show = ref(true)
2185+
onMounted(() => {
2186+
// trigger update
2187+
show.value = !show.value
2188+
})
2189+
return () =>
2190+
show.value
2191+
? (openBlock(), createElementBlock('div', { key: 0 }, 'show'))
2192+
: (openBlock(), createElementBlock('div', { key: 1 }, 'hidden'))
2193+
},
2194+
})
2195+
2196+
const AsyncComp = defineAsyncComp(() => {
2197+
const p = new Promise(resolve => {
2198+
resolve(Comp)
2199+
})
2200+
deps.push(p.then(() => Promise.resolve()))
2201+
return p as any
2202+
})
2203+
2204+
const toggle = ref(true)
2205+
const root = nodeOps.createElement('div')
2206+
const App = {
2207+
render() {
2208+
return (
2209+
openBlock(),
2210+
createElementBlock(
2211+
Fragment,
2212+
null,
2213+
[
2214+
h('h1', null, toggle.value),
2215+
toggle.value
2216+
? (openBlock(),
2217+
createBlock(
2218+
Suspense,
2219+
{ key: 0 },
2220+
{
2221+
default: h(AsyncComp),
2222+
},
2223+
))
2224+
: createCommentVNode('v-if', true),
2225+
],
2226+
PatchFlags.STABLE_FRAGMENT,
2227+
)
2228+
)
2229+
},
2230+
}
2231+
render(h(App), root)
2232+
expect(serializeInner(root)).toBe(`<h1>true</h1><!---->`)
2233+
2234+
await Promise.all(deps)
2235+
await nextTick()
2236+
await nextTick()
2237+
expect(serializeInner(root)).toBe(`<h1>true</h1><div>show</div>`)
2238+
2239+
await nextTick()
2240+
expect(serializeInner(root)).toBe(`<h1>true</h1><div>hidden</div>`)
2241+
2242+
// unmount suspense
2243+
toggle.value = false
2244+
await Promise.all(deps)
2245+
await nextTick()
2246+
expect(serializeInner(root)).toBe(`<h1>true</h1><!--v-if-->`)
2247+
})
2248+
2249+
test('unmount Suspense after async child (with async setup) self-triggered update', async () => {
2250+
const AsyncComp = defineComponent({
2251+
async setup() {
2252+
const show = ref(true)
2253+
onMounted(() => {
2254+
// trigger update
2255+
show.value = !show.value
2256+
})
2257+
const p = new Promise(r => setTimeout(r, 1))
2258+
// extra tick needed for Node 12+
2259+
deps.push(p.then(() => Promise.resolve()))
2260+
return () =>
2261+
show.value
2262+
? (openBlock(), createElementBlock('div', { key: 0 }, 'show'))
2263+
: (openBlock(), createElementBlock('div', { key: 1 }, 'hidden'))
2264+
},
2265+
})
2266+
2267+
const toggle = ref(true)
2268+
const root = nodeOps.createElement('div')
2269+
const App = {
2270+
render() {
2271+
return (
2272+
openBlock(),
2273+
createElementBlock(
2274+
Fragment,
2275+
null,
2276+
[
2277+
h('h1', null, toggle.value),
2278+
toggle.value
2279+
? (openBlock(),
2280+
createBlock(
2281+
Suspense,
2282+
{ key: 0 },
2283+
{
2284+
default: h(AsyncComp),
2285+
},
2286+
))
2287+
: createCommentVNode('v-if', true),
2288+
],
2289+
PatchFlags.STABLE_FRAGMENT,
2290+
)
2291+
)
2292+
},
2293+
}
2294+
render(h(App), root)
2295+
expect(serializeInner(root)).toBe(`<h1>true</h1><!---->`)
2296+
2297+
await Promise.all(deps)
2298+
await nextTick()
2299+
expect(serializeInner(root)).toBe(`<h1>true</h1><div>hidden</div>`)
2300+
2301+
// unmount suspense
2302+
toggle.value = false
2303+
await Promise.all(deps)
2304+
await nextTick()
2305+
expect(serializeInner(root)).toBe(`<h1>true</h1><!--v-if-->`)
2306+
})
2307+
2308+
test('propagates host el through wrapper components above Suspense after async child self-triggered update', async () => {
2309+
const AsyncComp = defineComponent({
2310+
async setup() {
2311+
const show = ref(true)
2312+
onMounted(() => {
2313+
show.value = false
2314+
})
2315+
const p = new Promise(r => setTimeout(r, 1))
2316+
deps.push(p.then(() => Promise.resolve()))
2317+
return () =>
2318+
h(
2319+
'div',
2320+
{ key: show.value ? 'show' : 'hidden' },
2321+
show.value ? 'show' : 'hidden',
2322+
)
2323+
},
2324+
})
2325+
2326+
const Inner = defineComponent({
2327+
render() {
2328+
return h(Suspense, null, {
2329+
default: () => h(AsyncComp),
2330+
})
2331+
},
2332+
})
2333+
2334+
const Outer = defineComponent({
2335+
render() {
2336+
return h(Inner)
2337+
},
2338+
})
2339+
2340+
const root = nodeOps.createElement('div')
2341+
const vnode = h(Outer)
2342+
render(vnode, root)
2343+
expect(serializeInner(root)).toBe(`<!---->`)
2344+
2345+
await Promise.all(deps)
2346+
await nextTick()
2347+
expect(serializeInner(root)).toBe(`<div>hidden</div>`)
2348+
2349+
const renderedEl = root.children[0]
2350+
const innerVNode = vnode.component!.subTree
2351+
const suspenseVNode = innerVNode.component!.subTree
2352+
2353+
expect(suspenseVNode.el).toBe(renderedEl)
2354+
expect(innerVNode.el).toBe(renderedEl)
2355+
expect(vnode.el).toBe(renderedEl)
2356+
})
2357+
21692358
test('should mount after suspense is resolved', async () => {
21702359
const target = nodeOps.createElement('div')
21712360

packages/runtime-core/src/componentRenderUtils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,13 +471,16 @@ function hasPropValueChanged(
471471
}
472472

473473
export function updateHOCHostEl(
474-
{ vnode, parent }: ComponentInternalInstance,
474+
{ vnode, parent, suspense }: ComponentInternalInstance,
475475
el: typeof vnode.el, // HostNode
476476
): void {
477477
while (parent) {
478478
const root = parent.subTree
479479
if (root.suspense && root.suspense.activeBranch === vnode) {
480-
root.el = vnode.el
480+
// Suspense proxies its active branch host node, so keep propagating from
481+
// the boundary vnode to any wrapper components above it.
482+
root.suspense.vnode.el = root.el = el
483+
vnode = root
481484
}
482485
if (root === vnode) {
483486
;(vnode = parent.vnode).el = el
@@ -486,4 +489,8 @@ export function updateHOCHostEl(
486489
break
487490
}
488491
}
492+
// also update suspense vnode el
493+
if (suspense && suspense.activeBranch === vnode) {
494+
suspense.vnode.el = el
495+
}
489496
}

0 commit comments

Comments
 (0)