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

Feature request: State management through a global store. #930

Closed
ispyinternet opened this issue Nov 17, 2017 · 17 comments
Closed

Feature request: State management through a global store. #930

ispyinternet opened this issue Nov 17, 2017 · 17 comments

Comments

@ispyinternet
Copy link

as redux is to react and vuex is to vue, I really believe svelte should have its own global state / store implementation that aligns with the core philosophies and concepts of the library.

People are continually questioning these topics in gitter. Nested components demand a data down actions up solution, and the current implementation of having to manually pass data down the chain and pass events back up the chain is lacking and I feel is crying out for a baked in solution. A number of us have hacked our own implementations using redux or similar external libraries, but I feel there is room for improvement if such a solution could be architected in the core that would still align with the pre-compiled no runtime paradigm?

If I knew where to begin I would but its beyond my station.

What are the thoughts of others who understand the challenge better?

@Rich-Harris
Copy link
Member

I agree, it would be nice to have an Answer (with a capital A) to this question. Existing solutions are totally workable, and I think there's value in allowing people to use whatever paradigm they're already comfortable with (which I think basically boils down to Redux vs Mobx?), but it doesn't feel like it has a seal of approval.

There are a couple of downsides to the current approach (subscribing to stores in lifecycle hooks):

  1. You initially render with no data (or placeholder data) then immediately update with values derived from the store's current state. That's wasteful.
  2. One of Svelte's built-in guards against doing unnecessary work is that set({ foo }) will only cause things that depend on foo to be updated (whether directly as in {{foo}}, or via computed values or component props etc). Anything that depends on bar is left alone. It doesn't work that way with Redux — instead, you get a whole new state object, and you don't know if the new state is a result of an action that changed foo or an action that changed bar. Svelte will do an equality check to prevent unnecessary updates, but it bails on objects and arrays since they could have been mutated.

The first problem could maybe be solved like this:

import store from './store';

export default {
  store,

  data({ store }) {
    // every time the store is updated, the component updates
    // like so: `component.set(data({ store }))`
    return {
      foo: store.foo
    };
  }
};

But that design wouldn't allow you to update properties conservatively the way it currently happens.

I'd be interested to hear what ideas people have for tackling this.

@Kiho
Copy link
Contributor

Kiho commented Nov 18, 2017

I like to PR related with this issue to make global store availabe to children on some scenario, it's intended to solve problem with redux-zero. Kiho@84775b3 it also give ability to initialize state to component, today I can do only if create it with code. I made sample usage here
https://github.com/Kiho/svelte-redux-zero/blob/context/src/store/store.ts
https://github.com/Kiho/svelte-redux-zero/blob/context/src/components/counter/Value.html
It give much cleaner redux-zero implementation, please review and let me know what you think.

@ispyinternet
Copy link
Author

ispyinternet commented Nov 23, 2017

Although the redux state object is new every time, the reference it holds will either be the same or not depending on whether it has changed so (as you guided not to mutate existing references)

state.b = 1;
state.a = [1,2,3];

// if an action updates b but leaves a untouched when the new state object is created...

newState.a === oldState.a

This is an important caveat on usage that facilitates reactive UIs and something I would expect a svelte implementation to enforce on the user to use it properly. Perhaps further performance can be found by using the ideas of a memoize library like reselect

Based on what 'the others are doing' I envisage that a svelte-state store should allow:

  1. Something along the lines of giving the store to the root component and expecting all child components to have a reference to it. I don't think importing a singleton into each child component is an accepted pattern.
  2. Succinct syntax for mapping parts of the store state tree to the component data object
  3. Succinct syntax for mapping parts of actions to methods.

I say something along the lines of, because if there were some syntax that signalled to the compiler you were using 'svelte-state', then perhaps the user doesn't even need to do anything, the compiler would hook up the shared state into all the child components.

Also since this is a 'builtin' store perhaps it need not have any external interface. Perhaps all its methods are only defined by the component methods, true also of the state shape coming only from component definitions? If the outside world wanted to dispatch an action on the state, it would have to do it through an interface of the component component.doStateAction()?

so perhaps components can have another property syntax similar to data() and methods() along the lines of stateData() and stateActions() and the compiler can do the magic?

Of course all the above ramblings are given with a completely naive understanding of what the svelte compiler can and cant do!

I don't know, just trying to dump my ideas...

@arxpoetica
Copy link
Member

arxpoetica commented Nov 23, 2017

I'll throw in my two cents, since I'm one of those Rumblers 🔥 you mentioned, @ispyinternet.

After a lot of rumbling, I finally went and utilized Redux-Zero, following @Kiho's helpful lead and other's advice. This is my implementation of a State Machine: https://gist.github.com/arxpoetica/fc16b1cdf3b552d761dd208d0bf5e917 If anyone has questions or wants to see more (it's not the whole yarn), I'm happy to comply.

Honestly it's not perfect, but I'm so far liking the very lesser amount of boilerplate—as long as I follow the constraints I put myself under.

I do like the idea of baking a store RIGHT IN Svelte itself. After having done a store, I can cite a few reasons why I think it makes sense:

  1. Svelte already manages state. Let me repeat that. Svelte is really good at managing state, it's just that it also lacks a lot of power by being a little too compartmentalized with components.
  2. With that in mind, it doesn't seem like Svelte actually needs to build a whole redux/flow/mobx/vuex system. Immutability is already handled internally, state is implicit within the context of components, and so forth and so on.
  3. As I see it, the main thing that's missing is an easy way for components to communicate state with each other.

One thing I learned from throwing a state machine on my app, I think it would be a mistake to have a second store built into Svelte. (I might sound like I'm contradicting myself, but I'm not.) It complicates the already existing state machine built in to Svelte. I'm not sure if this will be a popular view or not, but really I think the best thing that could happen would be to just have a really well built non-DOM-based API that allows one to wire up components at various levels—but one that doesn't easily lend itself to complicated spaghetti code. So to repeat myself, state is already baked. It just needs better tooling.

So far, the solution I've been using (which is actually a non-ideal separate store) doesn't feel spaghetti. It just "connects" endpoints, which respond gracefully if components move around and are easy to plug/unplug.

image

@Rich-Harris
Copy link
Member

Thanks everyone for the feedback and ideas. @Kiho I like where you're going with the this._root.options.store stuff — it definitely makes sense to automatically attach the component to the store without any extra work on the part of the component author.

I've sketched out an idea for what this could look like on a gist. Would love to hear what people think!

@Rich-Harris Rich-Harris mentioned this issue Nov 24, 2017
3 tasks
@Kiho
Copy link
Contributor

Kiho commented Nov 25, 2017

@Rich-Harris I am glad to hear that you liked my idea.
I was afraid about that could be wrong because I do not know internals of Svelte.
Anyway it's nice to help you to solve long standing issue in short time.

@arxpoetica
Copy link
Member

arxpoetica commented Nov 25, 2017

@Rich-Harris I'm really on the fence about the $ prefix. I love the rest of the ideas, tho.

I wonder if we do something more like $.? I realize this stinks to high heaven of jQuery syntax, but $whatever seems a really odd constraint. Just as an example, I almost always prefix vars that reference actual dom with $. $element = document.querySelector('element').

I think naming convention ought to not be on the var itself, but somehow otherwise encapsulated. Alternately it could be more explicit like $store.var? Dunno. Maybe that's too long, but it's also a bit more obvious to people unfamiliar with Svelte? Just trying to throw something non-breaking out there.

@arxpoetica
Copy link
Member

Also, there was some chatter on gitter (chitter? gatter?) regarding default state on the store. Just keeping a record of that over here too. Definitely think we need to be able to initialize the Store with some preloaded data. Common use case is to fetch JSON or whatever and populate an app while firing it up.

@Rich-Harris
Copy link
Member

Initial data works - the properties your component needs from the store are mixed in on initialisation.

I get what you mean about $. I didn't expect it to be completely uncontroversial... Ultimately I thought about something like $.foo or store.foo but that doesn't lend itself well to static analysis - you can't know (with solid guarantees, at least) which store properties a computed property depends on, for example, and the ability to do things like {{$[foo]}}would undermine important optimisations. So I eventually concluded that the prefix is the most favourable option - it's visually distinctive, easy to type and the dollar sign kind of looks like an 's' (for 'store'). Open to alternative suggestions though, if they retain the static analysis benefits!

I vote 'chitter'.

@Conduitry
Copy link
Member

The $ prefix is only used when you're using values in the store from the template or from computed properties, right? The 'actual' names on the store (which you use with get/set/etc.) don't have the prefix. I think the $ prefix is fine - it doesn't strike me as a naming convention, really - apart from no longer being able to start regular component data key names with a $.

@Rich-Harris
Copy link
Member

Yep, exactly:

store.set({ foo: 'bar' });
store.get('foo'); // 'bar'
store.get(); // { foo: 'bar' }
// etc

We'd probably want to start warning people that <Widget $foo='x'/> (and {{$foo}}, if compiled with store: false is going to cause problems in future.

@Rich-Harris
Copy link
Member

(Also, I had another thought re {{store.foo}} etc — that's potentially more confusing precisely because store is an object that has get and set methods etc, but no foo property.)

Rich-Harris added a commit that referenced this issue Nov 25, 2017
@Rich-Harris
Copy link
Member

Closing this — as of 1.43 we have svelte/store. Documentation here: https://svelte.technology/guide#state-management

@arxpoetica
Copy link
Member

Fantastic work, @Rich-Harris.

I'm still going to pick up on the $ if that's all right.

So using your example from above, namely:

store.set({ foo: 'bar' });
store.get('foo'); // 'bar'
store.get(); // { foo: 'bar' }

What if the vars actually are named:

store.set({ $foo: 'bar' });
store.get('$foo'); // 'bar'
store.get(); // { $foo: 'bar' }

Does that mean templates need to reference them so??? -->

<h1>Hello {{$$foo}}!</h1>

Sorry not really chiming in on useful alternatives. And maybe it's an unrealistic edge case. I don't tend to throw dollar signs on vars that get used in templates, but maybe that could happen and it would really throw someone off why $foo isn't working...?

@arxpoetica
Copy link
Member

arxpoetica commented Nov 27, 2017

Okay, offering up an option. I think the character ought to be a non-valid js character name/identifier.

So something like {{:foo}}, {{*foo}}, or {{~foo}}. You get my drift.

My vote? {{💰foo}} because it's easy 😂 to type and still looks like an s for store. And it's money.

Btw, just so I don't sound all critical, having read all the documentation, this is some top-caliber work. I'm very excited to put it to use ASAP.

@Rich-Harris
Copy link
Member

Invalid characters are impossible because Acorn wouldn't be able to parse them — same issue as #934 (comment). $ is the best option, I think.

Does that mean templates need to reference them so??? -->

<h1>Hello {{$$foo}}!</h1>

Yep! 😀

@arxpoetica
Copy link
Member

Okay, I'm satisfied. Thanks for letting me take a stroll. 😂 I hadn't realized you were throwing it in the JS parser. Good enough. :)

Awesome work.

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

5 participants