Skip to content

fix(suspense): avoid unmount activeBranch twice if wrapped in transition#9392

Merged
edison1105 merged 13 commits into
vuejs:mainfrom
edison1105:fix/7966
Mar 25, 2026
Merged

fix(suspense): avoid unmount activeBranch twice if wrapped in transition#9392
edison1105 merged 13 commits into
vuejs:mainfrom
edison1105:fix/7966

Conversation

@edison1105

@edison1105 edison1105 commented Oct 13, 2023

Copy link
Copy Markdown
Member

close #7966
the root cause is that the suspense resolves before the fallback node is mounted. This caused component B to be unmounted twice, causing this issue when it was unmounted the second time. see simplified demo
Notice the Console, unmount was printed twice.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented a double-unmount when using Suspense together with Transition in "out-in" mode so components are unmounted only once during fallback transitions.
  • Tests

    • Added an end-to-end test validating Suspense + Transition "out-in" behavior and ensuring the active branch is not unmounted twice.

@github-actions

github-actions Bot commented Oct 13, 2023

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 105 kB (+137 B) 39.7 kB (+32 B) 35.7 kB (+21 B)
vue.global.prod.js 163 kB (+137 B) 59.7 kB (+34 B) 53.1 kB (+19 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 48.2 kB 18.7 kB 17.2 kB
createApp 56.3 kB 21.8 kB 19.9 kB
createSSRApp 60.6 kB 23.5 kB 21.5 kB
defineCustomElement 62.5 kB 23.7 kB 21.6 kB
overall 70.9 kB (+137 B) 27.1 kB (+33 B) 24.7 kB (+26 B)

@pikax

pikax commented Oct 13, 2023

Copy link
Copy Markdown
Member

@edison1105 I don't think the solution is good enough, from what I could gather this is caused by the renderer.ts

Basically the unmount is being called multiple times on the vnode, I wonder if we could replace the condition on suspense with a non altering flag on suspense.

I suppose it would make sense the vnode to know it has been unmounted therefore if unmounted is called again it should be ignored, some input from @yyx990803 would be great :)

@edison1105

Copy link
Copy Markdown
Member Author

@pikax
That's a great idea. I changed the solution.

chore: improve code

chore: improve code

chore: improve code

chore(deps): update dependency @types/node to v18 (vuejs#9323)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

chore: improve code

chore: revert code

chore: improve code
@edison1105 edison1105 added the ready for review This PR requires more reviews label Aug 1, 2024
@edison1105 edison1105 added the 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. label Feb 18, 2025
@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a boolean flag isFallbackMountPending to SuspenseBoundary and adjusts Suspense resolve/fallback flows so active branch unmounting is deferred when a Transition in out-in mode schedules the fallback mount via afterLeave. Includes an e2e test that verifies the active branch is not double-unmounted.

Changes

Cohort / File(s) Summary
Runtime: Suspense boundary
packages/runtime-core/src/components/Suspense.ts
Added SuspenseBoundary.isFallbackMountPending: boolean. Modified resolve() to skip unmounting active branch while isFallbackMountPending is true and to clear the flag before switching branches; fallback() sets the flag and defers fallback mount via afterLeave. Initialization updated to false.
E2E test: Transition + Suspense
packages/vue/__tests__/e2e/Transition.spec.ts
Added an end-to-end test for a transition in out-in mode containing a Suspense timeout="0" that swaps branches and asserts DOM/classes and that onVnodeBeforeUnmount is called exactly once (no double-unmount).

Sequence Diagram(s)

sequenceDiagram
  participant App as Client
  participant T as Transition (out-in)
  participant S as Suspense
  participant R as Renderer/DOM

  App->>T: swap shallowRef (One -> Two)
  T->>S: trigger leave (out-in)
  S->>S: fallback() sets isFallbackMountPending = true\nattach afterLeave -> mountFallback
  T->>T: afterLeave fires
  T->>S: mountFallback (clear isFallbackMountPending = false)
  S->>R: mount fallback subtree
  S->>S: resolve() clears flag and swaps activeBranch (no double-unmount)
  S->>R: unmount old branch (once) and mount resolved branch
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 In tunnels of code I hop and sing,
Waiting on leaves, I hold this thing,
A flag I set when fallbacks pause,
So branches unmount just once because —
Hooray, no double-hop today! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR changes directly address issue #7966 by implementing a mechanism to prevent activeBranch double unmounting in Suspense+transition scenarios, which resolves the crash.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the Suspense double-unmount issue: core logic modifications in Suspense.ts and a focused e2e test validating the fix.
Title check ✅ Passed The title accurately and concisely describes the main fix: preventing double unmounting of the active branch when Suspense is wrapped in a transition with 'out-in' mode.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Mar 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@9392
npm i https://pkg.pr.new/@vue/compiler-core@9392
yarn add https://pkg.pr.new/@vue/compiler-core@9392.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@9392
npm i https://pkg.pr.new/@vue/compiler-dom@9392
yarn add https://pkg.pr.new/@vue/compiler-dom@9392.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@9392
npm i https://pkg.pr.new/@vue/compiler-sfc@9392
yarn add https://pkg.pr.new/@vue/compiler-sfc@9392.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@9392
npm i https://pkg.pr.new/@vue/compiler-ssr@9392
yarn add https://pkg.pr.new/@vue/compiler-ssr@9392.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@9392
npm i https://pkg.pr.new/@vue/reactivity@9392
yarn add https://pkg.pr.new/@vue/reactivity@9392.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@9392
npm i https://pkg.pr.new/@vue/runtime-core@9392
yarn add https://pkg.pr.new/@vue/runtime-core@9392.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@9392
npm i https://pkg.pr.new/@vue/runtime-dom@9392
yarn add https://pkg.pr.new/@vue/runtime-dom@9392.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@9392
npm i https://pkg.pr.new/@vue/server-renderer@9392
yarn add https://pkg.pr.new/@vue/server-renderer@9392.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@9392
npm i https://pkg.pr.new/@vue/shared@9392
yarn add https://pkg.pr.new/@vue/shared@9392.tgz

vue

pnpm add https://pkg.pr.new/vue@9392
npm i https://pkg.pr.new/vue@9392
yarn add https://pkg.pr.new/vue@9392.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@9392
npm i https://pkg.pr.new/@vue/compat@9392
yarn add https://pkg.pr.new/@vue/compat@9392.tgz

commit: cccb4a9

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/runtime-core/src/components/Suspense.ts`:
- Around line 566-570: The current logic skips unmounting
suspense.initialContent in resolve() but fallback() still flips isInFallback
before the fallback vnode is actually mounted, allowing patchSuspense() (which
checks pendingBranch && isInFallback) to treat the leaving initialContent as if
the fallback were active and unmount the same vnode twice; to fix, make the
isInFallback checks in patchSuspense() and any branches that act on
pendingBranch also verify that activeBranch !== suspense.initialContent (or that
suspense.initialContent is not the leaving node) before patching/unmounting, or
alternatively ensure unmount operations on the same vnode are idempotent at the
renderer level (adjust unmount logic used by patchSuspense(), resolve(), and
fallback() to guard against re-unmounting the same vnode).
- Line 651: The Suspense instance's initialContent remains set when the
fast-resolve path wins because resolve() replaces the afterLeave callback but
doesn't clear suspense.initialContent; update the resolve() logic in the
Suspense implementation (the resolve() method that interacts with
mountFallback() and afterLeave) to explicitly set suspense.initialContent = null
when the pending branch is replaced so the old vnode/component tree is released
once the new branch takes over.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f881373-2fcb-46cf-b0a8-d94e8eae6d1e

📥 Commits

Reviewing files that changed from the base of the PR and between d61d803 and 36ee2b5.

📒 Files selected for processing (2)
  • packages/runtime-core/src/components/Suspense.ts
  • packages/vue/__tests__/e2e/Transition.spec.ts

Comment thread packages/runtime-core/src/components/Suspense.ts Outdated
Comment thread packages/runtime-core/src/components/Suspense.ts Outdated
@edison1105 edison1105 changed the title fix(Suspense): avoid unmount activeBranch twice if wrapped in transition fix(suspense): avoid unmount activeBranch twice if wrapped in transition Mar 25, 2026
@edison1105 edison1105 added ready to merge The PR is ready to be merged. and removed ready for review This PR requires more reviews labels Mar 25, 2026
@edison1105

Copy link
Copy Markdown
Member Author

/ecosystem-ci run

@vuejs vuejs deleted a comment from edison1105 Mar 25, 2026
@vue-bot

vue-bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

📝 Ran ecosystem CI: Open

suite result latest scheduled
pinia success success
primevue success success
quasar failure success
language-tools failure failure
vite-plugin-vue success success
vue-i18n success success
vueuse success success
vuetify success success
vue-macros success success
nuxt success success
radix-vue success success
vitepress success success
vant success success
router success success
test-utils success success
vue-simple-compiler success success

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. ready to merge The PR is ready to be merged. scope: suspense

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App crashes when using router-view, transition, suspense and teleport

3 participants