Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kept alive components still update (renders, watchers) while being deactivated #5386

Open
posva opened this issue Feb 9, 2022 · 12 comments
Open
Labels
🐞 bug Something isn't working need guidance The approach/solution in the PR is unclear and requires guidance from maintainer to proceed further. scope: keep-alive

Comments

@posva
Copy link
Member

posva commented Feb 9, 2022

Version

3.2.30

Reproduction link

sfc.vuejs.org/

Steps to reproduce

  • Click increment
  • Click change page
  • Click increment
  • Observe the logs

What is expected?

  • Logs of One and then logs for Two (only)

What is actually happening?

After changing the page once, the Page One keeps rendering while being inactive


This has been confusing to many people but I couldn't find if this behavior is expected or not

This also means watchers are executed in deactivated branches

It also worth noting that the component being deactivated renders before being deactivated. Making it impossible to skip the render with likely incorrect data.

@posva posva added scope: keep-alive need guidance The approach/solution in the PR is unclear and requires guidance from maintainer to proceed further. labels Feb 9, 2022
@posva posva added the 🐞 bug Something isn't working label Jul 19, 2022
@posva posva changed the title Kept alive components update while being deactivated Kept alive components still update (renders, watchers) while being deactivated Jul 19, 2022
@BenceSzalai
Copy link

BenceSzalai commented Feb 28, 2023

I couldn't find if this behavior is expected or not

Can it be confirmed at least that these behaviour are indeed bugs and not intended?

From the layman's perspective they clearly are by the token that if someone wanted to keep the components rendering and watchers running they could use v-show to show/hide the component. The whole point of <KeepAlive> is to save resources in two ways:
1.) Avoid loosing the expensive/important state of the component, when switching away from it, as opposed to v-if and the like.
2.) Avoid taking resources for rendering and watching while in the background, as opposed to v-show="false".

The current behaviour matches that of option 2. Since it is already achievable quite easily with the v-show directive it suggest that it cannot be the goal of <KeepAlive>.

The documentation is rather vague on what the intended behaviour should be as it only really explains the behaviour as "cached". It explicitly says that the component state should be preserved (differentiating it from v-if etc.), but nothing about rendering, watchers or computed properties, which would differentiate it from v-show.

For me it looks like rendering, watchers or computed properties should be paused, otherwise we could simply just use v-show="false".

Correct me, if I'm wrong pls!

@yxchai
Copy link

yxchai commented Mar 1, 2023

The current behaviour matches that of option 2. Since it is already achievable quite easily with the v-show directive it suggest that it cannot be the goal of <KeepAlive>.

From my perspective, the rendering is not taken place as it's not rendered in the DOM tree, so it's not the same as v-show="false". While the official guide said below, I would rather say it's similar to v-show, but it won't stay in the DOM tree which means there will be less DOM manipulation(especially, if we are rendering large scale table, there will be a ton of DOM manipulation). Any maybe that's why it's said cache in the official guide. (cached in the virtual DOM, maybe...)

The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.

I also created a minimal playground locally but with Vue 2. From what I can tell, it works exactly the same as what @posva see in his playground.

So I suspect maybe the behavior is intended and not a bug. Let me know if you have different ideas. :-)

@nicolas-t
Copy link

Hello,

So I suspect maybe the behavior is intended and not a bug. Let me know if you have different ideas. :-)

Indeed if it's the same behavior as vue 2 then it might be intented,
The problem is other issues showing clearly not expected behaviors have been closed in favor of this issue :
#5323
#5207

@BenceSzalai
Copy link

BenceSzalai commented Mar 1, 2023

I would rather say it's similar to v-show, but it won't stay in the DOM tree which means there will be less DOM manipulation

That is a good point. Thanks!

If I look from this perspective than I can see these activity levels a component in theory could be in:

Level Component state Calculated, Watch etc. Rendering In the DOM Visible Current solution
1st 💡 In memory 🔆 Run ✅ Do ✅ Yes ✅ Yes ✅ Built in: Base state of mounted, active component
2nd 💡 In memory 🔆 Run ✅ Do ✅ Yes ❌ No ✅ Built in: v-show="false"
3rd 💡 In memory 🔆 Run ✅ Do ❌ No ❌ No ✅ Built in: <KeepAlive> + v-if="false"
4th 💡 In memory 🔆 Run ⏸ Paused ❌ No ❌ No ❗️ Inconvenient1: v-memo
5th 💡 In memory 💤 Sleep ⏸ Paused ❌ No ❌ No ‼️ Almost practically impossible2: v-memo + abortable watchers
6th 💣 Destroyed 🕳 N/A 🕳 N/A ❌ No ❌ No ✅ Built in: v-if="false"

note 1: Achieving rendering to halt using v-memo can be a bit cumbersome: one can make a Ref updated by a watcher that hashes together all the attributes that would be passed in the v-memo array, but also observes a control variable which can be used to abort any update to the v-memo control Ref when false effectively halting the rendering. It would be still much more convenient to have a simple wrapper, like <KeepRunning> + v-if="false", because the approach with v-memo forces the developer to explicitly collect and specify all dependencies, which is tiresome, prone to errors and creates maintenance overhead.

note 2: The even bigger problem is: sometimes we need 5th state as well, but currently it is not achievable in any reasonably simple ways. In fact one can replace all calculated variables with watchers and make those watchers observe a bail-out variable to abort any updates when desired, but it is a huge overhead to do it this way, also it must be combined with the v-memo solution from 4th level to also pause the rendering... And I'm not even sure if that would pause calculated attributes of child components. If not, those would need to be modified as well to observe the control variable. One must be desperate to do this.

In fact the name "KeepAlive" suggests something other than 5th or 6th state, as 6th obviously cannot be considered "Alive" and 5th neither based on that Watchers, Calculated and Rendering are disabled. So implementing 3rd or 4th state under the name <KeepAlive> makes sense.

On the other hand if the explanation in the documentation says "Cached", why the component is called "Alive"? Further complicating the matters the associated component state is called "inactive", and the associated lifecycle hook is called "deactivated". I know it happens a lot that names evolve organically, but I'd suggest these 4 different names are adding to the confusion. Also the term "cached" would be the perfect terminology to differentiate 3rd and 5th state: <KeepAlive>: same as right now, <KeepCached>: same as <KeepAlive> but with rendering and watchers+calculated paused.

In an ideal world I'd love to see convenient ways to do all 6 levels:

  • 1st: ✅ available as the base mounted state
  • 2nd: ✅ available using v-show="false"
  • 3rd: ✅ <KeepAlive> + v-if="false" (related state called: inactive)
  • 4th: 🆚 Achievable, but inconvenient => Would be better to have: <KeepRunning> + v-if="false" would be huge convenience. Related state could be called: background
  • 5th: 🆚 Achievable, but very inconvenient => Would be better to have: <KeepCached> + v-if="false" and related state could be called: cached
  • 6th: ✅ available as the base destroyed state e.g. with v-if="false"

@BenceSzalai
Copy link

BenceSzalai commented Mar 1, 2023

Now the terminology is hard to get right, because obviously existing names should be kept, also the same things may have different name from Component lifecycle perspective, documentation, developer experience representation etc.

Anyway here's my take on trying to collect my thoughts:

Aspect removed/disabled Lifecycle term Additional commonly used terminology3 Ideal state terminology4 Related markup
- mounted visible mounted -
Visible mounted visible => hidden mounted => hidden v-show="false"
In the DOM activated => deactivated active =>
inactive / cached
(any above) => alive <KeepAlive> + v-if="false"
Rendering n/a n/a (any above) => running <KeepRunning> + v-if="false"
Calculated, Watch etc. n/a n/a (any above) => cached <KeepCached> + v-if="false"
Component state * => unmounted created => destroyed (any above) => destroyed v-if="false"

note 3: These are the terms I found in Vue Dev Tools and the Documentation.

note 4: IMHO, indeed.

@tafelnl
Copy link

tafelnl commented Sep 13, 2023

I would argue your 5th 'level' describes the most logical and expected behaviour of them all, especially when combining it with vue-router. Suppose we have a simple app, with a "home"-page and another page (let's say "about us"). We use keep-alive in combination with vue-router to keep track of the entire state of the pages. If the user is on the "about us"-page, the "home"-page is invisible and vice versa.

Suppose the user navigated from "home" to "about us". Why would any developer want:

  • calculated stuff and watchers to continue on "home"? It just takes up user's resources (e.g. cpu and ram), and we're not going to show anything to the user based on these changing values. At most it can result in unexpected behaviour.
  • rendering to continue on "home". It doesn't make sense, as the page is hidden anyways

Am I missing a very obvious use-case here? The current behaviour of keep-alive - especially in combination with vue-router - just doesn't make any sense to me at all. Can anyone enlighten me?

@yxchai
Copy link

yxchai commented Sep 14, 2023

I would argue your 5h 'level' describes the most logical and expected behaviour of them all, especially when combining it with vue-router. Suppose we have a simple app, with a "home"-page and another page (let's say "about us"). We use keep-alive in combination with vue-router to keep track of the entire state of the pages. If the user is on the "about us"-page, the "home"-page is invisible and vice versa.

Suppose the user navigated from "home" to "about us". Why would any developer want:

* calculated stuff and watchers to continue on "home"? It just takes up user's resources (e.g. cpu and ram), and we're not going to show anything to the user based on these changing values. At most it can result in unexpected behaviour.

* rendering to continue on "home". It doesn't make sense, as the page is hidden anyways

Am I missing a very obvious use-case here? The current behaviour of keep-alive - especially in combination with vue-router - just doesn't make any sense to me at all. Can anyone enlighten me?

@tafelnl , I think it relates to this #7286 pull request. I believe once this PR is merged. We will see this bug being fixed in vue-router soon.

@tafelnl
Copy link

tafelnl commented Sep 18, 2023

@yxchai I think that's a totally different issue to be honest. I am talking about pausing computed and watch and pausing rendering after navigating away from a route. That issue talks about invoking the activated hook for child components

@Alfred-Skyblue
Copy link
Member

I tried pausing the effect in the keep-alive deactivate state in #9206. It can be achieved currently, but it is more difficult in vShow because vShow needs to execute the effect before it can determine whether to display it.

@angelov-a
Copy link

The issue with keeping the state reactive is very much concerning the app's mode of operation.

We are using KeepAlive for routing and each view opens a modal on a specific url. The modal itself registers a keystroke listener for the Esc key which navigates back to a predetermined url. Now the deactivated view also detects that it needs to setup the modal, which upon pressing Esc triggers navigation to the wrong page (the deactivated one).

@mefcorvi
Copy link
Contributor

mefcorvi commented Aug 8, 2024

I doubt that the current behavior will ever be changed because it would be a massive breaking change: there is no guarantee that there aren't projects that rely on reactivity continuing to work inside deactivated components.

The very concept of KeepAlive implies that a component is added to the vDOM/DOM faster because it is kept in memory. If reactivity within deactivated components stops working, then when the component is added back to the vDOM/DOM, it would be almost like creating the component anew, as all computed properties would need to be recalculated and the vDOM/DOM re-rendered.

Additionally, what should be done in situations where, for example, there is a watch in the component on a value from the Vuex store, and the component increments a counter every time this value changes? Should we remember all the changes and replay them upon reactivation of the component? This example is far-fetched, but in real applications, the logic can be much more complex.

It must be admitted that in our project, we did indeed encounter situations where reactivity within deactivated components (in our case, entire pages) led to performance drops. We solve this with two custom composables: useActivatableEffect and computedActivatable. All reactivity inside useActivatableEffect is removed when the component is deactivated and recreated upon activation. We also use this for registering event listeners. The computedActivatable function uses useActivatableEffect internally to create a computed property that only recalculates when the component is active.

I cannot share the code for these composables, mainly because we handle many different nuances related to SSR and hydration, and we also perform a lot of other necessary magic specific to our case. However, fundamentally, these are very simple functions. Here is an example implementation in the SFC Playground.

@angelov-a
Copy link

@mefcorvi Thanks for the explanation. I was thinking of using KeepAlive hooks to control reactivity, but thought that there may be a more general solution.

I see that #9651 implements pause/resume while mentioning KeepAlive although there are no changes in the PR related to the latter, as far as I can tell. Will the new methods have to be explicitly used (together with activated/deactivated) in order to achieve results similar to your examples?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐞 bug Something isn't working need guidance The approach/solution in the PR is unclear and requires guidance from maintainer to proceed further. scope: keep-alive
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants