Skip to content

Svelte really needs a keep-alive feature similar to Vue #6040

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

Closed
kay999 opened this issue Mar 2, 2021 · 15 comments
Closed

Svelte really needs a keep-alive feature similar to Vue #6040

kay999 opened this issue Mar 2, 2021 · 15 comments

Comments

@kay999
Copy link

kay999 commented Mar 2, 2021

Is your feature request related to a problem? Please describe.

In the moment writing any kind of component switching (tabs, router, etc) can become quite difficult, because Svelte removes the component state if it is hidden via {$if} or svelte:component. So if you switch back the old state is destroyed unless it's stored somewhere else outside the component (a writable for example).

Describe the solution you'd like
In Vue there is a special tag which signals to not remove the component state but just unmount the DOM. So after switching back. the framework can easily restore the old state from the stored date. Something similar would be very useful in Svelte too.

In Svelte there could be a new svelte:keep-alive tag which changes the way the inner components are created/destroyed. By writing something like

<svelte:keep-alive><svelte:component ..../><svelte:keep-alive>

components could be switched without losing state.

Describe alternatives you've considered
I tried two ways: Letting the DOM live and using "display:none" to only switch it's visibility or use a store to store the component state externally to restore it afterwards.

Both solutions aren't ideal.

Letting the DOM alive can result in lots of DOM nodes which aren't really used but are still updated in the background after changes. This is in conflict with Sveltes otherwise quite efficient execution model.

Using stores to externalize state makes the application much harder to write. If you've created your application without the problem in mind, it may require huge changes afterwards. Doing it right requires wrapping lots of fields in writables, and registering them somewhere to reload them after re-mounting a component. This is not easy to do it correct and leads to much harder to understand code.

Isn't it the job of the framework to solve those problems? Doing all the state management by hand is a clear contradiction to the otherwise quite programmer-friendly model of Svelte.

How important is this feature to you?
I consider it quite important for big applications. In the moment I'm not sure how to continue with my current project because of the mentions drawbacks given above.

@antony
Copy link
Member

antony commented Mar 2, 2021

The DOM isn't a place to store state, and Svelte discourages it. A keepalive would be in direct conflict with this model.

The goal of Svelte is to automatically render a view which is in-sync with your data model, and reactively re-render that view if the model changes.

If your state's "source of truth" is inside your navigation component, that signals that data is already flowing in the wrong direction. A navigation component should be reading state from above it in the application, since links and such to change pages would need to be setting that state.

I feel like the problem you're describing is a symptom of a problem with the way you're storing state in your application.

Using stores to externalize state makes the application much harder to write

Moving state to a more central place where it sits above its consumers makes the application slightly harder to write but vastly easier to reason about.

If you've created your application without the problem in mind, it may require huge changes afterwards

Adding a special tag as a workaround for applications that have been built in a sub-optimal way is not really a problem that we'd look to solve. As a developer, as your application iterates, it's often necessary to refactor various parts as it takes shape.

If you can provide a worked example of what you mean, it might become clearer, but right now it feels like your state sounds like it is being stored in the wrong place, and the flow of your data isn't quite right, which is why you're looking for a workaround.

@antony antony added the awaiting submitter needs a reproduction, or clarification label Mar 2, 2021
@kay999
Copy link
Author

kay999 commented Mar 2, 2021

Looking in the Discord I've found lots of question regarding this feature. So I'm not the only one who's missing it.

The point is that Svelte isn't stateless. And I consider this a feature because it makes writing components much easier. In React they saw this too and created the concept called hooks to solve it. I've also used real stateless VDom libs (like snabbdom) and it gets quite ugly if you want to write composable and maintainable code. Not because of the state of your data-model which is of course external but because of the ui-state which also needs to be maintained and bound to the right components.

So I came to Svelte, really liking it's component model where I don't have to think about all this - just to discover, that it fails at some point, missing an important feature.

An example:

Consider creating a 'tabbed' component where you can switch between tabs via svelte:component. Those tab-components implement "forms" which are loaded on onMount from a DB. This data is loaded into the component to edit it via field-components. But if you edit data in one form, then switch to another and then back, the component is mounted again and the data is reloaded, overwriting your edits.

Those forms aren't part of the data-model, they display temporary ui-data until the user does a submit which writes the data back to the data-model. This kind of ui-data should be stored in the component according to the way Svelte works and according to the examples. And as long I use a single form-component without putting it in tabs, everything would be perfectly fine. But in the moment I put it in my tab-component, my forms won't work anymore. I consider this a violation of the principle of composability.

Now in this example I can workaround the problem by implementing the tab-component by switching via DOM display:none. But that's IMO not a good solution because it would require holding all tabs in the DOM which wouldn't scale well.

Another way would be to store the form-state in a global store and let the components edit parts of them. But if you've written the forms in the "canonical" way (as here for example) using locals to store the state of the form-fields, you need to a big refactroring of your code. You also need to decide where to store the state and when to remove it.

One problem is that you can't write "standalone" form-components anymore, you always need external infrastructure to manage it's state externally. While this is certainly possible, why should this be a concern of the programmer?

And it's not bad design to store temporary data in a component. Svelte does this all over the place and I consider this a good thing because of my experience with real stateless VDom. And it generally works quite well.

@dummdidumm
Copy link
Member

I can see the point that it's desireable to have state that is not visible still around. The example you mentioned with forms is something I also experienced. But in my case, even if the form was big and spread throughout steps, it was never a problem to just use display:none. Still I agree that this does not scale to infinity.

Just a quick thought:

<svelte:outlet target={aDomNode}>
   stuff that is rendered into the target. If the target is undefined, it's rendered nowhere but the things inside still "live".
</svelte:outlet>

Somewhat related concepts: Angular ng-template and React portals.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

Moving state to a more central place where it sits above its consumers makes the application slightly harder to write but vastly easier to reason about.

This is true if you share state (which is generally true for the data models). But it is not true for non-shared ui-state!

If you have to manage the state of every component in your UI (like "is a drop-down menu currently open", "which is the current-selected row in a list", "is a fold-component open or hidden", etc) you don't gain any clarity or ease. In fact you complicate your code massively (unless you have some clever abstractions to manage it).

This is why React has hooks (or component-objects earlier) and why Svelte allows you to write very compact code.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

But in my case, even if the form was big and spread throughout steps, it was never a problem to just use display:none. Still I agree that this does not scale to infinity.

True. And I will probably go this way too because it's much to cumbersome to rewrite everything to externalize the relevant ui-state, optimizing only the real big components somehow.

Still it's annoying that Svelte can't handle this case more easily. In Vue it wouldn't be easy to solve (but Vue has other problems which Svelte solved much better).

At least there should be some kind of warning in the docs. And at least an example of the canonical way to write forms in a way this problem wouldn't happen.

In the moment you implement your forms freely away - just to discover that they lose their state after you've put them into a router and that there is no easy way around it.

@antony
Copy link
Member

antony commented Mar 2, 2021

In the moment you implement your forms freely away - just to discover that they lose their state after you've put them into a router and that there is no easy way around it.

Reading your points above, it all appears to boil down to the above statement, and I think my original analysis still holds.

You talk about "ui state" which is akin to "storing state in the dom".

The dom isn't a place to store state. It's a view over your state. The reason it's not a good place to store state is in your original issue - because when you delete it, the state is lost. That's because it's not a good place to store state.

The correct way to do the multi-page form you talked about before would be to create a store, place it in the form's context, and bind your inputs to it. You can then freely remove components at will, and the state will persist. It's very much the correct place to store state, and it's bounded (using set/get context) to the form.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

The dom isn't a place to store state. It's a view over your state.

This is not the way Svelte works and advertises itself. And that's the reason I use Svelte. Because this "view over your state" doesn't scale and doesn't compose well. I've tried it.

Also why does Swelte has (and promotes in every example) internal state, if it's unnecessary or even bad?

because when you delete it, the state is lost

Yes, and this is intended! If I open a CRUD form to edit some data, it's temporary and its state should be lost if I close it.

But those forms may be switched temporarily by the UI to make usability better. Displaying all data in one big monster-form isn't ideal and resource friendly. And if I implement for example a calculator app or a notification list in a different page, I don't want my form to lose its edits just because I quickly switched over it and then back.

In the moment I have to store state in the DOM because otherwise it would be lost, resulting in poor UX. Which BTW breaks Sveltes animation-system which doesn't work anymore if I use display:none.

place it in the form's context, and bind your inputs to it.

Yes, this would work. But it would also complicate things unnecessarily. And it would create additional external dependencies and shared mutated state, making the design of the app worse.

I also has other drawbacks: Do I put all fields in one store or do I create a map of stores which each stores the fields? The first is easier but every change invalidates the whole form now, so the second one should be used. But then I have a big object containing multiple stores which I can't use as easily as "normal" values, because I now have to wrap/unwrap every access to every field. Lots of boilerplate now instead of clear design.

So why does svelte a clever compiler-based "invalidate" mechanism on local state, if I shouldn't use it?

@antony
Copy link
Member

antony commented Mar 2, 2021

This is not the way Svelte works and advertises itself.

"Svelte writes code that surgically updates the DOM when the state of your app changes"

Because this "view over your state" doesn't scale and doesn't compose well. I've tried it.And that's the reason I use Svelte.

Then Svelte might not be a good fit for your use-case or patterns and you might be better off with something else. FWIW "view over state" absolutely does scale and composes fantastically.

Also why does Swelte has (and promotes in every example) internal state, if it's unnecessary or even bad?

Because internal (component) state is absolutely what you should use where possible. However if you need the component to be conditional and the state persisted, then you should be storing the state somewhere else - simply - in the parent, or for more complex use cases - in a store.

because when you delete it, the state is lost

Because you are storing state in something that you then delete. That's completely logical.

Yes, and this is intended! If I open a CRUD form to edit some data, it's temporary and its state should be lost if I close it.

When you close it, clear the the thing that holds the state (most likely, a store)

But those forms may be switched temporarily by the UI to make usability better. Displaying all data in one big monster-form isn't ideal and resource friendly. And if I implement for example a calculator app or a notification list in a different page, I don't want my form to lose its edits just because I quickly switched over it and then back.

How you display state is of no relevance, and is entirely up to you. If you need data to persist over "page" (view) changes, then store it in a place which doesn't get disposed (in contrast to your suggestion that there should be a mechanism to not dispose of things which get removed)

In the moment I have to store state in the DOM because otherwise it would be lost, resulting in poor UX. Which BTW breaks Sveltes animation-system which doesn't work anymore if I use display:none.

That's right. Don't use the DOM as a store of state. That's not what it's for. It's for rendering state (at least in the Svelte model, along with numerous other libraries)

Yes, this would work. But it would also complicate things unnecessarily. And it would create additional external dependencies and shared mutated state, making the design of the app worse.

I fail to see how using a store to store state is more complicated than storing it in transient / uncontrolled dom elements and hoping it sticks around. The design of an app which stores state correctly is infinitely better than the alternatives.

State is designed to be mutated, that's how reactivity works.

I also has other drawbacks: Do I put all fields in one store or do I create a map of stores which each stores the fields? The first is easier but every change invalidates the whole form now, so the second one should be used.

Stores are intentionally simple. They're just hashes of data which you can subscribe to to observe mutations. How you structure a hierarchy of these is up to you, and how best fits your application. Storing state in "buckets" (i.e grouped into a number of dependent stores) is a great way to ensure that cross-field validity is synced when a value changes.

But then I have a big object containing multiple stores which I can't use as easily as "normal" values, because I now have to wrap/unwrap every access to every field. Lots of boilerplate now instead of clear design.

This is akin to saying sticking your entire app in a single file is clear design and having to import other files is boilerplate. Technically you're right but there's a reason we don't do things like this.

Store states in stores. Let the dom be "surgically updated" when store values change. That's about as simple and crystal clear as it can get.

So why does svelte a clever compiler-based "invalidate" mechanism on local state, if I shouldn't use it?

I'm not 100% sure what you mean by this, to be honest.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

Then Svelte might not be a good fit for your use-case or patterns and you might be better off with something else.

Svelte generally works fantastic for me. Because it allows local mutable component state. My problem is not Svelte it's that certain things in Svelte aren't implemented completely.

FWIW "view over state" absolutely does scale and composes fantastically.

That's the Elm-way of thinking. I'm talking about Svelte.

I fail to see how using a store to store state is more complicated than storing it in transient / uncontrolled dom elements and hoping it sticks around.

Because you have to manage it. And it's not the Svelte-way of doing things.

Nearly every example on svelte.dev uses local-mutable state. So it's obviously the way it's done in Svelte.

Of course you need to distinguish between data-model and ui-component state. data-model-state is clearly external and should be. But ui-component-state is always implemented as mutable local-state in the examples.

If I want to compose an application correctly I build components which uses components etc. But components often have internal complexities which should be hidden. For example if I implement a 'popup-component', would you want to call it as

<Popup value={currentValue}>...</Popup>

or as

<Popup popupIsOpen={popupState} value={currentValue}>...</Popup>

?

The first one is clearly better because it shouldn't matter to the caller of "Popup" if there even is a "Popup-State" and how to manage it.

If you agree, why shouldn't the same be true for a form? Why shouldn't I call it for example as

<CustomerData customerId={id}/>

Instead of

<CustomerData data={customerData}/>

? Why should the caller even now, how customerData ist fetched, or written back? And is "data" even complete, maybe it should contain various additional state which is only relevant for the ui and has nothing to do with the data model. Why should the caller think about it? It would be the same as expecting to supply popupState above.

Composable components should expose as less internals as possible. The first way would do this perfectly, it's totally up to CustomerData how data is fetched, written, displayed, etc. The second one is a kludge to circumvent conceptual problems of the framework.

And in Svelte using the first way works nearly perfectly. Only if you put the component into some "switch" component, it breaks down. Of course it's still possible to write components in the first way by building some internal state-management-system storing state in some global store. But why do I even need to build this when Svelte could in principle handle it automatically?

And again: It's not about storing things in the DOM, it's about letting a Svelte component "live" while it's DOM is deleted. So the component could be remounted and rebuild it's DOM without requiring the programmer to do all the state management.

Stores are intentionally simple.

Yes, and this is good. But it also means that you can only changes stores "as one" (at least from Sveltes perspective) which requires even more boilerplate code. To do it correctly the "customerData" above need to have a store for every data-field. Possible, of course. But why bother the programmer with this kind of bookkeeping?

Store states in stores. Let the dom be "surgically updated" when store values change. That's about as simple and crystal clear as it can get.

Then why don't the examples on svelte.dev don't do this and use let state = ... instead? Most of the examples don't use any store, they always store state in locals via let.

I'm not 100% sure what you mean by this, to be honest.

If you look at the generated code, you see the invalidation mechanism of changed locals in action. The compiler detects which locals are changed and generate code like

	function input_change_handler() {
		visible = this.checked;
		$$invalidate(0, visible);  
	}

The $$invalidate(0, visible); is the mechanism I'm talking about - which would be totally unnecessary if you are right and state should always be managed by stores.

Again: You describe the Elm-Way of doing UIs. Svelte works differently and the examples and tutorials show this clearly.

@dummdidumm
Copy link
Member

I disagree with your view of "the Svelte way". Just because the tutorials and examples do not make heavy use of global state does not mean it's discouraged. The tutorials and examples are little snippets where it would be overkill to use global state, that's why they are not used there. I also disagree that by using Stores you are automatically doing the "elm way", you just lift the state management one level up.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

But why do the examples in a way which won't work in real applications? And in fact this way work very well implementing UI elements (and forms are UI elements, just as a popup-menu is).

Of course if different UI elements share state, you need to use an adequate solution. That what stores are for. But why should I make state artificially shared only to implement some persistence mechanism by hand to circumvent a small oversight in the design of the framework? That's exactly the opposite of good design.

Vue is very similar to Svelte in many regards. All points brought up in this discussion are equally true for Svelte and Vue. And Vue has this "keep-alive" feature. Why do you think, they did that?

@antony
Copy link
Member

antony commented Mar 2, 2021

That's the Elm-way of thinking. I'm talking about Svelte.

I've never used Elm, I've used Svelte extensively for the last four years. I refer you again to the sentence written on the Svelte homepage - "Svelte writes code that surgically updates the DOM when the state of your app changes."

You seem to be fixated on the fact that because Svelte allows you to keep component state locally, and the fact that there are references to this in the documentation and examples, that it's the de-facto, and only way to store state. It's an excellent way to store state until you need slightly more extensible state management. For this we provide stores. May I therefore point you at the store documentation, which is also detailed in the examples and Svelte documentation:

https://svelte.dev/docs#svelte_store

Your use case describes a common scenario where local state is no longer sufficient for your needs and where you should move required state into stores. Rather than trying to bend component local state into something which it is not designed for, and requesting the Svelte API to be extended with facilities to allow you to use it for a purpose it is not suitable for, may I suggest that you instead use the mechanism that we provide for this specific use-case among others.

And again: It's not about storing things in the DOM, it's about letting a Svelte component "live" while it's DOM is deleted

The only reason to need this is because you're storing state in an unsuitable place for your use case (the dom/component)

Of course it's still possible to write components in the first way by building some internal state-management-system storing state in some global store.

No need to build your own state management state. We provide one, called Stores.
No need for it to be global, you can control its bounds using setContext/getContext

But why do I even need to build this when Svelte could in principle handle it automatically?

It does. It's called Svelte Stores. You're looking for a different mechanism to preserve state by shoehorning it into a place that doesn't make sense, and persisting it using a mechanism which needlessly allows you to have that floating around in your application after it should have been destroyed by the component lifecycle.

if you are right and state should always be managed by stores.

I am right, but you keep reading what I'm writing in black and white for some reason. Local state where suitable, Stores where you need something more complex. The two mechanisms are not mutually exclusive.

But components often have internal complexities which should be hidden.

internal complexities = local state
external complexities = store state (or bound state)

I'm not sure why you keep fixating on using one or the other. Use each where appropriate. Don't put your entire app into stores. Just put state which is required outside of a single component or parent-child component into stores.

Then why don't the examples on svelte.dev don't do this

As @dummdidumm said. Simplicity for examples. But also because it's the right way. The data being stored is only relevant to the component. Look at the mapbox example. Stores + Context.

But why do the examples in a way which won't work in real applications

Because they are examples, focussed on and intentionally simplified to demonstrate a specific concept without the complexity of everything else getting in the way.

why should I make state artificially shared only to implement some persistence mechanism

putting state in a store doesn't make it shared, it allows it to persist outside of the component lifecycle, which is precisely what you are asking for in this feature request.

Why do you think, they did that?

more importantly, who cares? They implemented the feature because they saw a need for it. we have a different mechanism, which also works well. We don't implement features because other libraries have them, we implement features based on a need for them. in this instance, using Svelte the way it was designed to be used would solve your use-case.

@antony antony added proposal and removed awaiting submitter needs a reproduction, or clarification labels Mar 2, 2021
@kay999
Copy link
Author

kay999 commented Mar 2, 2021

You seem to be fixated on the fact that because Svelte allows you to keep component state locally, and the fact that there are references to this in the documentation and examples, that it's the de-facto, and only way to store state.

No, but from the example (and my own experience using Svelte) I consider this the canonical way for non-shared local ui-state.

As I stated for shared UI-state, external storage is of course necessary and I've used stores extensively for it.

But what is so bad about creating a form this way:

<form>
   Name: <input bind:value={name}>
   Address: <input bind:value={address}>

   <button on:click={submit}>Submit</button>
</form>

<script>
   export let id;
   let name = '', address = '';
  
   function submit( ) {
     put('url/' + id, { name, address });
  } 
   onMount(async () => 
       const r = await get('url/' + id);
       name = r.name;
       address = r.address;
   );
</script>

It's a self contained, compact, well readable component. Why should I bother to make this clean design more complex by externalizing state?

It's an excellent way to store state until you need slightly more extensible state management.

But what if I don't want to manage my state and only want to let components stay alive even if they are invisible for some time? And this isn't some strange use-case, every SPA uses it.

The only reason to need this is because you're storing state in an unsuitable place for your use case (the dom/component)

I don't consider it an "unsuitable place" but in fact the most suited one. As in the "Popup" example: Local state belongs as close to the component as possible.

No need to build your own state management state. We provide one, called Stores.

Those stores don't create and store them self. They must be supplied, created, destroyed. I call this "management".

you can control its bounds using setContext/getContext

But why should I need to think about that? Why can't I create a self-contained component and just use it supplying the necessary arguments and nothing more?

In fact it's possible to do that in Svelte - until you want to switch for example via a router to a different page for short time and back without losing component state.

Why not simply be able to declare components on use as "store the state" instead of requiring the programmer to rewrite an otherwise completely fine program?

I'm not sure why you keep fixating on using one or the other. Use each where appropriate.

Exactly that's what I want. But then I lose page-switching without losing state in my program. Which isn't an option because of UX.

Just put state which is required outside of a single component or parent-child component into stores.

Exactly that's my point: I don't want to expose state which isn't required outside just to make page-switching possible.

I want clear design, no workarounds just to make the framework happy.

@kay999
Copy link
Author

kay999 commented Mar 2, 2021

Because they are examples, focussed on and intentionally simplified to demonstrate a specific concept without the complexity of everything else getting in the way.

And where are the real-world examples? But why even make it possible to implement it this way when it's not intended, not the "Svelte-way"? Shouldn't the examples show how a framework should be used?

putting state in a store doesn't make it shared,

Of course. And without sharing, it wouldn't be a solution to the stated problem. It's shared between the component and some persistence mechanism (which only consists of a single reference to the store, but nonetheless...).

But the big question remains: Why does Svelte have mutable local state and a clever and well thought out mechanism of observing changes of this state, it it's supposedly "unnecessary" and not the intended way to do it? And why do most examples and also most component libs I've looked at extensive use of this mechanism if it's so bad?

which is precisely what you are asking for in this feature request.

I ask for a simple solution which fits well into the framework.

They implemented the feature because they saw a need for it. we have a different mechanism, which also works well.

No, you don't. That's the problem. Vue also has stores like Svelte (implemented a bit differently but could be used quite similar) and they could also propose a very similar solution as yours above. Still they decided to create the keep-alive feature because they deemed it necessary.

@pngwn
Copy link
Member

pngwn commented Mar 2, 2021

This is a significant enough feature request that it warrants an RFC. Please create an RFC at https://github.com/sveltejs/rfcs in order to discuss this feature request further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants