diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts
index 8de5b3182d0..1509eed3e93 100644
--- a/packages/runtime-core/__tests__/components/Suspense.spec.ts
+++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts
@@ -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'
@@ -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(`
fallback
`)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(``)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(``)
+ })
+
test('mount the fallback content is in the correct position', async () => {
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts
index e9723f23652..0326e33123a 100644
--- a/packages/runtime-core/src/components/Suspense.ts
+++ b/packages/runtime-core/src/components/Suspense.ts
@@ -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 {