Skip to content

fix(runtime-core): skip idle persisted transition hooks in keep-alive moves#14865

Merged
edison1105 merged 5 commits into
vuejs:mainfrom
LeSingh1:fix/14031-keep-alive-persisted-transition
May 27, 2026
Merged

fix(runtime-core): skip idle persisted transition hooks in keep-alive moves#14865
edison1105 merged 5 commits into
vuejs:mainfrom
LeSingh1:fix/14031-keep-alive-persisted-transition

Conversation

@LeSingh1
Copy link
Copy Markdown
Contributor

@LeSingh1 LeSingh1 commented May 20, 2026

Fixes #14031.

When a <keep-alive> activates or deactivates a cached child, the renderer's move function calls transition.beforeEnter / transition.enter (or leave) on every element vnode that carries a transition. For a normal transition this is correct, but for a persisted transition (the flag the template compiler injects when <Transition> has a single v-show child) the lifecycle is owned by the directive, not by mount/move. The element is being moved in or out of a detached storage container; the visibility is already controlled by v-show.

As a result, every cache hit was firing onBeforeEnter / onEnter and (because no async hook keeps it pending) onAfterEnter, even when the v-show target was never displayed. The reproduction in the issue uses a v-show="false" element inside a <keep-alive> and observes after-enter logged on each toggle.

The fix extends the same !transition.persisted guard that mountElement already uses (via the module-level needTransition helper) to the per-element branch inside move. Persisted transitions still get their hostInsert so the element is correctly relocated between the live tree and the keep-alive storage container, but the directive-owned enter / leave hooks are no longer called behind the directive's back. The existing #13153 interaction with _isLeaving lives outside the persisted path and is unaffected.

Added a regression test in BaseTransition.spec.ts that wraps a KeepAlive switching between two components inside a BaseTransition with persisted: true and asserts that none of the enter or leave hooks fire on activate or deactivate. The test fails on main (the deactivate path calls onBeforeLeave / onLeave and the activate path calls all three enter hooks via the immediate done() in BaseTransition) and passes with the change.

Summary by CodeRabbit

  • Bug Fixes

    • Persisted (cached) transitions now properly skip enter/leave lifecycle hooks when components are activated/deactivated, preserving DOM placement and avoiding unexpected animation callbacks.
    • Renderer now ensures persisted elements are inserted/removed without triggering redundant transition callbacks.
  • Tests

    • Added a test confirming cached component switches do not invoke transition lifecycle hooks.
    • Adjusted an existing transition test to account for a longer transition duration.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Renderer move() now skips enter/leave hooks for persisted transitions; a unit test verifies persisted transitions inside KeepAlive do not fire enter/leave during cached component activation/deactivation, and an e2e test's transition timing was parameterized to match a longer duration.

Changes

Persisted Transition Behavior Fix

Layer / File(s) Summary
Renderer: skip hooks for persisted moves/leaves
packages/runtime-core/src/renderer.ts
Move path: persisted transitions use hostInsert only on enter; leave path: persisted transitions call remove() directly, skipping leave and afterLeave chaining.
Test: BaseTransition #14031
packages/runtime-core/__tests__/components/BaseTransition.spec.ts
Adds a test that mounts a persisted BaseTransition inside KeepAlive, switches branches to deactivate/reactivate cached components, and asserts no enter/leave lifecycle hooks are invoked during those cycles.

E2E Transition test timing update

Layer / File(s) Summary
E2E: parameterize duration and adjust waits
packages/vue/__tests__/e2e/Transition.spec.ts
Parameterizes the browser evaluate call with duration, sets the inner Transition :duration="${duration * 4}", and updates transitionFinish() to transitionFinish(duration * 4) to align test timing with the explicit duration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • vuejs/core#13152: Also adjusts renderer.ts transition handling and pending leave cancellation around KeepAlive edge cases.
  • vuejs/core#14443: Addresses guards around preventing enter when a vnode is already leaving; related to enter/leave ordering and overlap fixes.

Suggested labels

scope: transition, :hammer: p3-minor-bug

Suggested reviewers

  • edison1105
  • johnsoncodehk

Poem

🐰 I hopped through branches, soft and spry,
Where kept-alive leaves no noisy cry.
Hooks stay quiet, gentle as a stream,
Cached petals rest in leafy dream.
A rabbit cheers — the tests are calm and shy.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title directly and accurately summarizes the main change: fixing persisted transition hooks to skip during keep-alive moves, which is the core fix for issue #14031.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

Open in StackBlitz

@vue/compiler-core

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

@vue/compiler-dom

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

@vue/compiler-sfc

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

@vue/compiler-ssr

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

@vue/reactivity

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

@vue/runtime-core

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

@vue/runtime-dom

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

@vue/server-renderer

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

@vue/shared

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

vue

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

@vue/compat

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

commit: 660751e

Comment thread packages/runtime-core/src/renderer.ts Outdated
shapeFlag & ShapeFlags.ELEMENT &&
transition
transition &&
!transition.persisted
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes because the fix does address #14031, but the current blanket guard regresses the existing #13153 KeepAlive + pending v-show leave behavior.

@edison1105 edison1105 added 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. wait changes labels May 26, 2026
Address @edison1105's review: the previous blanket `!transition.persisted`
guard on `needTransition` regressed vuejs#13153 because it routed persisted
leaves through the bare `hostInsert` fallback, skipping the
`_isLeaving` / `leaveCbKey` cancellation that vuejs#13153 added.

Restructure the move() branch so persisted transitions still take the
needTransition path (preserving the _isLeaving cancellation in
performLeave) but skip only the directive-owned beforeEnter/enter and
leave/afterLeave calls. Existing vuejs#14031 test still passes; full
runtime-core + runtime-dom suites green (1245/1246, one pre-existing
skip).
@LeSingh1
Copy link
Copy Markdown
Contributor Author

Thanks for catching that, @edison1105 — you're right, the !transition.persisted guard on needTransition routed persisted leaves through the bare hostInsert fallback and skipped the _isLeaving / leaveCbKey cancellation from #13153.

Pushed ff7a800: kept persisted transitions on the needTransition path so the if (el!._isLeaving) leaveCbKey(true) still runs in performLeave, but skipped only the directive-owned hook calls — beforeEnter / enter on MoveType.ENTER, and leave / afterLeave on MoveType.LEAVE. Persisted leaves now just do remove() after the cancellation.

The #14031 test still passes, and full runtime-core + runtime-dom suites are 1245/1246 green (one pre-existing skip). I didn't add a unit-level #13153 regression test since the existing e2e in packages/vue/__tests__/e2e/Transition.spec.ts:1846 (move kept-alive node before v-show transition leave finishes) already covers the _isLeaving / display: none invariant — let me know if you'd prefer a node-ops one too.

@edison1105 edison1105 force-pushed the fix/14031-keep-alive-persisted-transition branch from 2e53ace to 9ee1b75 Compare May 27, 2026 07:18
@edison1105 edison1105 changed the title fix(runtime-core): skip persisted transition hooks when moving kept-alive nodes (fix #14031) fix(runtime-core): skip idle persisted transition hooks in keep-alive moves May 27, 2026
@edison1105 edison1105 added ready to merge The PR is ready to be merged. and removed wait changes labels May 27, 2026
@edison1105
Copy link
Copy Markdown
Member

edison1105 commented May 27, 2026

Thanks, I pushed a follow-up that keeps the #14031 persisted-transition skip narrow to cases where there is no pending v-show leave.

see 9ee1b75

@github-actions
Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 106 kB (+79 B) 40.1 kB (+33 B) 36 kB (+39 B)
vue.global.prod.js 164 kB (+79 B) 60.1 kB (+29 B) 53.4 kB (-21 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 48.8 kB (+79 B) 19 kB (+27 B) 17.4 kB (+19 B)
createApp 56.9 kB (+79 B) 22 kB (+27 B) 20.1 kB (+21 B)
createSSRApp 61.2 kB (+79 B) 23.8 kB (+27 B) 21.7 kB (+22 B)
defineCustomElement 63.1 kB (+79 B) 23.9 kB (+27 B) 21.8 kB (+35 B)
overall 71.7 kB (+79 B) 27.4 kB (+28 B) 25 kB (-10 B)

@edison1105
Copy link
Copy Markdown
Member

/ecosystem-ci run

@vuejs vuejs deleted a comment from edison1105 May 27, 2026
@vue-bot
Copy link
Copy Markdown
Contributor

vue-bot commented May 27, 2026

📝 Ran ecosystem CI: Open

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

@edison1105 edison1105 merged commit 80fc139 into vuejs:main May 27, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. ready to merge The PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

after-enter event triggered even when transition wrapped in keep-alive never shows its child

3 participants