Subscribe on changes!

Transitions with v-if statements at root-level of components aren't working the same way as in Vue 2

avatar
Feb 4th 2023

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.

avatar
Feb 7th 2023

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

avatar
Feb 7th 2023

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

avatar
Feb 7th 2023

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

avatar
Feb 7th 2023

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.

avatar
Feb 7th 2023

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
avatar
Feb 7th 2023

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

image
avatar
Feb 8th 2023

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):

https://github.com/AlexVagrant/core/blob/fix/transition_v-if/packages/runtime-core/src/components/BaseTransition.ts#L227-L228

avatar
Feb 9th 2023

@AlexVagrant great find! Are you working on a PR to fix this?

avatar
Feb 9th 2023

@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.

avatar
Sep 7th 2023

@yyx990803 would be great to see this merged, so we can finally remove the workarounds that we needed to add when transitioning from vue2 to vue3…