Skip to content

fix(Suspense): avoid double resolve during patch suspense #11471

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 2 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
61 changes: 60 additions & 1 deletion packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import {
watch,
watchEffect,
} from '@vue/runtime-test'
import { computed, createApp, defineComponent, inject, provide } from 'vue'
import {
Transition,
computed,
createApp,
defineComponent,
inject,
provide,
} from 'vue'
import type { RawSlots } from 'packages/runtime-core/src/componentSlots'
import { resetSuspenseId } from '../../src/components/Suspense'

Expand Down Expand Up @@ -1441,6 +1448,58 @@ describe('Suspense', () => {
expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`])
})

test('branch switch during suspense patching', async () => {
const toggle = ref(true)

const Async1 = defineAsyncComponent({
async setup() {
// switch to Async2
toggle.value = false
return () => h('div', 'async1')
},
})

const Async2 = defineAsyncComponent({
async setup() {
return () => h('div', 'async2')
},
})

const route = computed(() => {
return toggle.value ? [Async1] : [Async2]
})

const Comp = {
setup() {
provide('route', route)
return () =>
h(RouterView, null, {
default: ({ Component }: any) => [
h(Suspense, null, {
default: () =>
h(Transition, null, {
default: () => h('div', null, [h(Component)]),
}),
fallback: h('div', 'fallback'),
}),
],
})
},
}

const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)

await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div><!----><!----></div>`)

await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div><!----><div>async2</div></div>`)
})

test('mount the fallback content is in the correct position', async () => {
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
Expand Down
50 changes: 28 additions & 22 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,28 +248,34 @@ function patchSuspense(
slotScopeIds,
optimized,
)
if (suspense.deps <= 0) {
suspense.resolve()
} else if (isInFallback) {
// It's possible that the app is in hydrating state when patching the
// suspense instance. If someone updates the dependency during component
// setup in children of suspense boundary, that would be problemtic
// because we aren't actually showing a fallback content when
// patchSuspense is called. In such case, patch of fallback content
// should be no op
if (!isHydrating) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
namespace,
slotScopeIds,
optimized,
)
setActiveBranch(suspense, newFallback)
// #7506 pendingBranch may be unmounted during patching. If so,
// resolve may be triggered and pendingBranch will be set to null.
// Therefore, we need to check that pendingBranch is not null here
// to avoid a double resolve.
if (suspense.pendingBranch) {
if (suspense.deps <= 0) {
suspense.resolve()
} else if (isInFallback) {
// It's possible that the app is in hydrating state when patching the
// suspense instance. If someone updates the dependency during component
// setup in children of suspense boundary, that would be problemtic
// because we aren't actually showing a fallback content when
// patchSuspense is called. In such case, patch of fallback content
// should be no op
if (!isHydrating) {
patch(
activeBranch,
newFallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
namespace,
slotScopeIds,
optimized,
)
setActiveBranch(suspense, newFallback)
}
}
}
} else {
Expand Down