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(`
async2
`) + }) + 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 {