Skip to content

fix(Suspense): update Suspense vnode's el during branch self-update #12922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 140 additions & 1 deletion packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
KeepAlive,
Suspense,
type SuspenseProps,
createBlock,
createCommentVNode,
createElementBlock,
h,
nextTick,
nodeOps,
onErrorCaptured,
onMounted,
onUnmounted,
openBlock,
ref,
render,
resolveDynamicComponent,
Expand All @@ -23,9 +26,17 @@ import {
watch,
watchEffect,
} from '@vue/runtime-test'
import { computed, createApp, defineComponent, inject, provide } from 'vue'
import {
computed,
createApp,
defineAsyncComponent as defineAsyncComp,
defineComponent,
inject,
provide,
} from 'vue'
import type { RawSlots } from 'packages/runtime-core/src/componentSlots'
import { resetSuspenseId } from '../../src/components/Suspense'
import { PatchFlags } from '@vue/shared'

describe('Suspense', () => {
const deps: Promise<any>[] = []
Expand Down Expand Up @@ -2161,6 +2172,134 @@ describe('Suspense', () => {
await Promise.all(deps)
})

// #12920
test('unmount Suspense after async child (with defineAsyncComponent) self-triggered update', async () => {
const Comp = defineComponent({
setup() {
const show = ref(true)
onMounted(() => {
// trigger update
show.value = !show.value
})
return () =>
show.value
? (openBlock(), createElementBlock('div', { key: 0 }, 'show'))
: (openBlock(), createElementBlock('div', { key: 1 }, 'hidden'))
},
})

const AsyncComp = defineAsyncComp(() => {
const p = new Promise(resolve => {
resolve(Comp)
})
deps.push(p.then(() => Promise.resolve()))
return p as any
})

const toggle = ref(true)
const root = nodeOps.createElement('div')
const App = {
render() {
return (
openBlock(),
createElementBlock(
Fragment,
null,
[
h('h1', null, toggle.value),
toggle.value
? (openBlock(),
createBlock(
Suspense,
{ key: 0 },
{
default: h(AsyncComp),
},
))
: createCommentVNode('v-if', true),
],
PatchFlags.STABLE_FRAGMENT,
)
)
},
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<h1>true</h1><!---->`)

await Promise.all(deps)
await nextTick()
await nextTick()
expect(serializeInner(root)).toBe(`<h1>true</h1><div>show</div>`)

await nextTick()
expect(serializeInner(root)).toBe(`<h1>true</h1><div>hidden</div>`)

// unmount suspense
toggle.value = false
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<h1>true</h1><!--v-if-->`)
})

test('unmount Suspense after async child (with async setup) self-triggered update', async () => {
const AsyncComp = defineComponent({
async setup() {
const show = ref(true)
onMounted(() => {
// trigger update
show.value = !show.value
})
const p = new Promise(r => setTimeout(r, 1))
// extra tick needed for Node 12+
deps.push(p.then(() => Promise.resolve()))
return () =>
show.value
? (openBlock(), createElementBlock('div', { key: 0 }, 'show'))
: (openBlock(), createElementBlock('div', { key: 1 }, 'hidden'))
},
})

const toggle = ref(true)
const root = nodeOps.createElement('div')
const App = {
render() {
return (
openBlock(),
createElementBlock(
Fragment,
null,
[
h('h1', null, toggle.value),
toggle.value
? (openBlock(),
createBlock(
Suspense,
{ key: 0 },
{
default: h(AsyncComp),
},
))
: createCommentVNode('v-if', true),
],
PatchFlags.STABLE_FRAGMENT,
)
)
},
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<h1>true</h1><!---->`)

await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<h1>true</h1><div>hidden</div>`)

// unmount suspense
toggle.value = false
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<h1>true</h1><!--v-if-->`)
})

describe('warnings', () => {
// base function to check if a combination of slots warns or not
function baseCheckWarn(
Expand Down
8 changes: 6 additions & 2 deletions packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,13 +451,13 @@ function hasPropsChanged(
}

export function updateHOCHostEl(
{ vnode, parent }: ComponentInternalInstance,
{ vnode, parent, suspense }: ComponentInternalInstance,
el: typeof vnode.el, // HostNode
): void {
while (parent) {
const root = parent.subTree
if (root.suspense && root.suspense.activeBranch === vnode) {
root.el = vnode.el
root.suspense.vnode.el = root.el = vnode.el
}
if (root === vnode) {
;(vnode = parent.vnode).el = el
Expand All @@ -466,4 +466,8 @@ export function updateHOCHostEl(
break
}
}
// also update suspense vnode el
if (suspense && suspense.activeBranch === vnode) {
suspense.vnode.el = el
}
}