Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 10 additions & 2 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,13 @@ function patchSuspense(
// 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) {
// The same applies while the fallback mount is deferred to the leaving
// branch's afterLeave (out-in transition): activeBranch is still the
// leaving content, not the fallback. Patching it with the fallback here
// would hijack activeBranch, and resolve() would then attach the
// content move to an afterLeave that never fires, permanently dropping
// the resolved branch.
if (!isHydrating && !suspense.isFallbackMountPending) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
patch(
activeBranch,
newFallback,
Expand Down Expand Up @@ -316,7 +322,9 @@ function patchSuspense(
)
if (suspense.deps <= 0) {
suspense.resolve()
} else {
} else if (!suspense.isFallbackMountPending) {
// while the fallback mount is deferred to the leaving branch's
// afterLeave, activeBranch is still the leaving content — see above
patch(
activeBranch,
newFallback,
Expand Down
73 changes: 73 additions & 0 deletions packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,79 @@ describe('e2e: Transition', () => {
E2E_TIMEOUT,
)

// a parent update arriving while the timed-out fallback swap is waiting
// for the leaving branch's afterLeave must not drop the resolved branch
test(
'parent update during timed-out fallback swap with out-in transition',
async () => {
await page().evaluate(() => {
const { createApp, shallowRef, ref, h, defineAsyncComponent } = (
window as any
).Vue
const One = {
setup() {
return () => h('div', { class: 'test' }, 'one')
},
}
const Two = {
async setup() {
return () => h('div', { class: 'test' }, 'two')
},
}
const AsyncTwo = defineAsyncComponent(
() =>
new Promise(res => {
;(window as any).resolveTwo = () => res(Two as any)
}),
)
createApp({
template: `
<div id="container">
<transition mode="out-in" :duration="300">
<Suspense :timeout="10">
<component :is="view" :key="name" :data-tick="tick"/>
<template #fallback><div class="fallback">loading</div></template>
</Suspense>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
<button id="tickBtn" @click="tick++">tick</button>
`,
setup: () => {
const view = shallowRef(One)
const name = ref('one')
const tick = ref(0)
const click = () => {
view.value = AsyncTwo
name.value = 'two'
}
return { view, name, tick, click }
},
}).mount('#app')
})
await transitionFinish()
expect(await html('#container')).toBe(
'<div class="test" data-tick="0">one</div>',
)

// navigate to the cold async branch; timeout=10 starts the fallback
// swap and the old branch's leave transition
await click('#toggleBtn')
await timeout(10 + buffer)
// unrelated parent re-render while the fallback mount is deferred to
// the leaving branch's afterLeave
await click('#tickBtn')
// the async branch resolves while the leave is still in progress
await page().evaluate(() => (window as any).resolveTwo())
await transitionFinish(300)
await transitionFinish(300)
await nextTick()
expect(await html('#container')).toContain('two')
expect(await html('#container')).not.toContain('loading')
},
E2E_TIMEOUT,
)

// #14640
test(
'switch suspense branches after teleport updates before pending mount finishes',
Expand Down