Reactivity maybe be broken with nested computed and vue-router
Vue version
3.4.15
Link to minimal reproduction
https://github.com/CamilleDrapier/vue-test/
Steps to reproduce
- checkout the provided repo
- run
npm install
- run
npm run dev
- open your favorite browser on the provided local URL
- open the web console of your browser
- Click on the
Unknown
button/tab
What is expected?
- A log with "To the unknown~" should appear (i.e.: when unmounting the page, the application should detect that the new route is not a "isAnyRoute" anymore).
- This log should appear each time when the route is transitioned between "home" and "unknown" afterward.
What is actually happening?
- No log with "To the unknown~"
System Info
System:
OS: Linux 6.5 KDE neon 5.27 5.27
CPU: (12) x64 Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz
Memory: 12.17 GB / 30.97 GB
Container: Yes
Shell: 5.8.1 - /usr/bin/zsh
Binaries:
Node: 20.11.0 - ~/.nvm/versions/node/v20.11.0/bin/node
npm: 10.2.4 - ~/.nvm/versions/node/v20.11.0/bin/npm
Browsers:
Chrome: 121.0.6167.85
npmPackages:
vue: ^3.4.15 => 3.4.15
Any additional comments?
This repo comes with a couple of extra branches:
vue-3.3.13
that tries to confirm that the same code works as expected with Vue3.3.13
(please runnpm install
and rerun your server after switching branches)is-home
which uses a slightly less-nestedisHome
computed, which seems to work as expected.no-watch
, which comments out a watcher on the "offending isAnyRoute" computed. The application seems to work as expected.
This problem could be related to other reactivity issues that were reported on the 3.4.X
branch, but it is a bit difficult for me to say if it is a duplicate of another issue. I could confirm that the same problem happens as well in 3.4.13
, so maybe it is different than https://github.com/vuejs/core/issues/10185 but might still be the same "narrow edge case" that is mentioned over there.
Sorry in advance for this complex use case; if anything it might not affect many users, and it can probably be worked around if need be~ :bow:
'To the unknown~'Shouldn't it be triggered when the value of isAnyRoute.value is false? I don't see any exception here. Is it a problem with the Linux system?
Thanks for looking and helping with this matter!
I doubt that this is an Operating System specific problem, as I can reproduce the problem with the repository I provided on my Windows 10 computer. Also, the problem I'm facing with my real-life application has been reported/confirmed by Linux, Windows, and Mac users.
Sorry for my previous lack of explanation, the problem here is that the isAnyRoute.value
is true
, while the internal log for the computed isAnyRoute
reports that its constituent computed properties (isHome
, isAbout
) are both false
. Thus accessing isAnyRoute.value
should be false
after isAnyRoute
has been computed for the last time.
- When navigating from the "home" page to the "unknown" page, you can see the following logs:
isAnyRoute false false
, this is expected because the new route is neither 'home' nor 'about', and the computedisAnyRoute
is being calculated because the route it relies on has changed (and because the result is displayed n the page).Unmounted Home - isAnyRoute: true
, this is unexpected, because the new return value that was just previously calculated isfalse
- If you try the same code with version
3.3.13
(by using the branchvue-3.3.13
on the provided repo for example, and of course runnpm install
andnpm run dev
again) you can see:isAnyRoute false false
, again: this is expected/normal - but it is triggered twice, so maybe in the latest version there is an optimization that prevents this re-calculation.Unmounted Home - isAnyRoute: false
, this time the computed value from the "using component" has the expectedfalse
value.To the unknown~
, since the previous value is false, this log appears.
Hopefully, this helps clarify my previous report; sorry for the convoluted reproduction pattern, I tried trimming as much as I could!
The double triggering of isAnyRoute false false
might be due to the combination of <p>{{ useRouteHelpers().isAnyRoute }}</p>
and the onUnmounted
hook. When I attempted to comment out the watch, the reactivity system behaved normally. I hope this insight can provide some ideas for others trying to troubleshoot the issue.
https://stackblitz.com/~/github.com/ikkkp/vue-test-main
FYI Calling composables inside the render function is wrong usage and should be avoided. Composables should only be called once per component, inside setup()
or <script setup>
.
Thanks for the additional insight, and sorry for this improper use of composables in my example!
I have updated my example repository which produces the small problem I mentioned in my original post. It should not have calls to composable inside the render/template anymore. And I also got rid of irrelevant warnings. Hopefully, this makes it easier to understand the problem. :bow:
My guess is this is because in 3.4, chained computed
becomes lazy evaluated - it doesn't compute until it is needed, but by the time in onUnmounted
, the first-level computed effects are already stopped and never triggered the chained computed to become dirty. I will need to look a bit deeper to confirm this though.
/cc @johnsoncodehk
@yyx990803 @johnsoncodehk I believe this is a timing issue about the computed
disposed before the onUnmounted
cb invoke and I think it is reasonable.
Mount HomeView and get
isAnyRoute
.Click unknown and cause HomeView unmount, but scope stop will occurred before invoke
um
hooks. Stop scope will stop the effect registered in it as well, here includeisAnyRoute
. When the stop happened inisAnyRoute
, it will cleanup thedeps
ofisAnyRoute
.Unmounted cb invoke
console.log('Unmounted Home - isAnyRoute:', isAnyRoute.value); // isAnyRoute is MaybeDIrty
if (!isAnyRoute.value) console.log('To the unknown~');
HereisAnyRoute
is MaybeDirty
but it was cleaned up though and will return NotDirty, so it will return cached value.
So, @CamilleDrapier I think it's not a good practice to use effects in onUnmouted
hooks, since it may be disposed, and I believe it will work in onBeforeUnmount
hooks in your case.
Oops, found Johnson has a PR for this one after I commented 😂
anyway, onBeforeUnmount
is at least a temporary workaround
Might be related, but we also encountered this issue on our in-house UI Component Library where a component was provided router link props.
Here is the behavior minimally reproduced:
On Vue 3.3.13 it works: playground
On Vue 3.4.19 it doesnt (resulting in Uncaught (in promise): Cannot read properties of undefined (reading 'resolve')
): playground
(I had to use Vuetify Playground bc Vue SFC Playground wasnt behaving as expected no matter which version 3.3/3.4)