Transitions with v-if statements at root-level of components aren't working the same way as in Vue 2
Vue version
3.2.45
Link to minimal reproduction
https://stackblitz.com/edit/vue-3-transition-with-root-level-v-if
Steps to reproduce
Compare with the same reproduction for Vue 2:
The reproduction displays a button that each time it is clicked, it cycles a counter between 1..2..3.
For each number, a component is rendered. One
, Two
and Three
.
Component Two
has a v-if="false"
statement at root-level, essentially not rendering the component at all.
What is expected?
In Vue 2, once the counter reaches 3, component Three
is rendered with a transition as expected, just like when the v-if
is applied outside of component Two
when it is rendered, e.g. with a chainged v-if
, v-else-if
statement nested in the Transition
.
Screen-cast of the expected behavior:
vue-2-transition-with-root-level-v-if.mp4
What is actually happening?
In Vue 3, once the counter reaches 3, component Three
is not rendered at all, the transition appears to be remaining in a confused state. Only once the counter goes back to 1, the transition starts working again.
Screen-cast of the actual behavior:
vue-3-transition-with-root-level-v-if.mp4
System Info
System:
OS: macOS 13.1
CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
Memory: 2.57 GB / 32.00 GB
Shell: 5.8.1 - /bin/zsh
Binaries:
Node: 19.6.0 - ~/.nvm/versions/node/v19.6.0/bin/node
Yarn: 3.3.1 - ~/.nvm/versions/node/v19.6.0/bin/yarn
npm: 9.4.0 - ~/.nvm/versions/node/v19.6.0/bin/npm
Browsers:
Chrome: 109.0.5414.119
Chrome Canary: 112.0.5578.0
Firefox: 109.0.1
Firefox Nightly: 111.0a1
Safari: 16.2
Safari Technology Preview: 16.4
Any additional comments?
While this particular example may seem a bit nonsensical, this situation can be encountered in a real scenario, e.g. if the component rendered uses v-if
at root-level to only render its contents once some data is fetched asynchronously. In Vue 2, such a component would still apply a transition once it is ready to be rendered. This is broken in Vue 3 not only for the component itself, but even for the next component that would render itself after the one that uses v-if
in such a way.
This isn't a vue3 bug. if you use out-in mode in transition, you need to use <Suspense>
tag to wrap your tag.
<template>
<button @click="handleAddCounter">Step</button>
to cycle through counter: {{ counter }} name is {{ names[counter] }}
<div class="transition">
<Transition name="slide-up" mode="out-in">
<Suspense>
<component :is="names[counter]" :key="counter"></component>
</Suspense>
</Transition>
</div>
</template>
I find this issue resolution from https://github.com/vuejs/core/blob/main/packages/vue/__tests__/e2e/Transition.spec.ts#L1375
Thank you @AlexVagrant for pointing this out, but I am not sure it's save to use <Supsense>
? From the docs:
<Suspense>
is an experimental feature. It is not guaranteed to reach stable status and the API may change before it does.
https://vuejs.org/guide/built-ins/suspense.html
And if this is indeed the solution, then it should be mentioned in the Migration Guide
Follow up: It does indeed solve this problem (Update: that's sadly not the case, see below), but note that the docs only mention async
dependencies, not this particular use-case. It's definitely not something one would naturally expect to solve this problem:
https://stackblitz.com/edit/vue-3-transition-with-root-level-v-if-and-suspsense
Oh, and when using this feature, you end up with such messages in the console:
<Suspense> is an experimental feature and its API will likely change.
While this does work for the reduced test-case I posted above, in the code base that I am porting from Vue 2 to Vue 3, <Suspense>
does not actually replace the old functionality correctly. I end up with lots of these errors:
Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core
at <Transition mode="out-in" onBeforeLeave=fn<onBeforeLeave> ... >
Uncaught (in promise) DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
at insert (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:7943:12)
at move (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:6459:7)
at move (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:6414:7)
at activeBranch.transition.afterLeave (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:2576:15)
at performRemove (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:6529:20)
at remove2 (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:6541:7)
at unmount (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:6496:9)
at Object.resolve (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:2589:11)
at patchSuspense (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:2515:18)
at Object.process (http://localhost:3000/_nuxt/node_modules/.vite/deps/chunk-3NMN3MUW.js?v=cc5bdc30:2399:7)
insert @ runtime-dom.esm-bundler.js:10
move @ runtime-core.esm-bundler.js:6094
move @ runtime-core.esm-bundler.js:6043
activeBranch.transition.afterLeave @ runtime-core.esm-bundler.js:1394
performRemove @ runtime-core.esm-bundler.js:6184
remove2 @ runtime-core.esm-bundler.js:6200
unmount @ runtime-core.esm-bundler.js:6142
resolve @ runtime-core.esm-bundler.js:1405
patchSuspense @ runtime-core.esm-bundler.js:1326
process @ runtime-core.esm-bundler.js:1204
patch @ runtime-core.esm-bundler.js:5125
componentUpdateFn @ runtime-core.esm-bundler.js:5729
run @ reactivity.esm-bundler.js:190
instance.update @ runtime-core.esm-bundler.js:5763
callWithErrorHandling @ runtime-core.esm-bundler.js:173
flushJobs @ runtime-core.esm-bundler.js:406
Promise.then (async)
queueFlush @ runtime-core.esm-bundler.js:298
queueJob @ runtime-core.esm-bundler.js:292
(anonymous) @ runtime-core.esm-bundler.js:5761
triggerEffect @ reactivity.esm-bundler.js:400
triggerEffects @ reactivity.esm-bundler.js:390
trigger @ reactivity.esm-bundler.js:358
set2 @ reactivity.esm-bundler.js:543
set @ runtime-core.esm-bundler.js:3220
onIntersection @ DeferredRenderingMixin.js:101
handle @ intersection.js:83
Ok, click the Step
button repeatedly at high frequency in the new version of the reduced test-case that include <Suspense>
here, and you'll get the same errors. This definitely doesn't fix this bug:
https://stackblitz.com/edit/vue-3-transition-with-root-level-v-if-and-suspsense
Hi guy, I know how to solve this bug.
It's a Transition
bug, when we use v-if
in Transition
and the v-if
result is false, Vue will insert a comment node to replace origin component, it is a placeholder node.
Transition
get all children from slots, but the <!--v-if-->
comment can't get from slots, this is the reason.
The following is a simple solution (not the final one):
@AlexVagrant great find! Are you working on a PR to fix this?
Yes, I will fix this bug soon, please give me a little time.