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

Nested stores #1435

Closed
Rich-Harris opened this issue May 7, 2018 · 25 comments
Closed

Nested stores #1435

Rich-Harris opened this issue May 7, 2018 · 25 comments

Comments

@Rich-Harris
Copy link
Member

There are lots of situations where it's useful for a subtree of components to have their own local store. For example, a <Chart> component might want to have things like points and scaling functions that can be accessed by child components without prop drilling:

<svg class="chart">
  <!-- these automatically render nice axes based on
       the extents of the data, and the dimensions of the chart -->
  <Axis horizontal/>
  <Axis vertical/>

  <Scatterplot/>
  <VoronoiOverlay maxRadius={50} on:select="$set({ selected: event.point })"/>
</svg>

While it is possible for any subtree to have its own store...

export default {
  store: () => new MyLocalStore({
    points: [{ date: ..., value: ... }, ...],
    x: linearScale(...),
    y: linearScale(...)
  })
};

...that makes it impossible for components in that subtree to access properties of the global store, which is often necessary.

A possible solution:

<!-- Chart.html -->
export default {
  store: parentStore => parentStore.child()
};

With this, this.store in <Chart> and all its children would refer to the child store. $someGlobalProperty would still work, but so would $someLocalProperty.

Details/questions:

  1. Does it become the same kind of store as the parent, if the parent is a subclass of Store? Do we need to be able to specify the class of the child store? (Probably)
  2. What does $set({ somePreviouslyUnknownProperty }), or a binding to the same property, do? Does it modify the local store or the parent store? One possibility would be to make it explicit which properties belong to the child:
// like this...
parentStore.child({
  props: ['someLocalProperty']
})

// ...or this (which I think I prefer)
parentStore.child({
  someLocalProperty: 42,
  someOtherThingThatIsALocalPropertyButDoesntHaveAKnownValueYet: undefined
})
  1. Do we need a way to map parent properties to child properties? My gut says we can file this under YAGNI; among other things, it would make the 'only respond to actual changes' optimisation impossible, and would make things altogether more confusing.
@dxlbnl
Copy link
Contributor

dxlbnl commented May 7, 2018

I like the idea, but thinking about multiple stores, I think there should be another pattern possible.
So currently I'm splitting store logic into multiple files, which chain into an inheritance tree:

// storeLogic[n].js
export default BaseStore =>
  class Store extends BaseStore {
  }
// store.js
import { Store as BaseStore } from 'svelte/store.js'
import storeLogic1 from './storeLogic1.js'
import storeLogic2 from './storeLogic2.js'

const Store = storeLogic2(storeLogic1(BaseStore))

But this way the individual stores could still clash, so it would be nice if stores could be composed without having variables clash (namespaced i guess)

Ideas:

  • store.sub('namespace', subStoreClass)
    • `this.store.get('namespace').var | {$namespace.var}
    • And having stores not worry about conflicting names
      • By being able to: this.set({ var: 42}) instead of this.set({namespaceVar: 42})

Doesn't solve how a subset of components could attach their store to the global store, without having to worry where their state is coming from.

@jacwright
Copy link
Contributor

To allow child stores to subclass Store I think better API choices would be:

/* Chart.html */
export default {
  store: parentStore => parentStore.addChild(new Store())
};

or even better (IMO):

/* Chart.html */
export default {
  store: parent => new Store({}, { parent })
};

The latter sets the parent of a store, which makes more sense since this store is the one all subcomponents will use and it happens to have a parent. The other API makes it feel like you are adding a child to a parent store and makes it feel like that parent store may have many children which can be accessed from anywhere in the app, which is not the case.

Both these syntaxes allow for both stores to be subclassed.

@Rich-Harris
Copy link
Member Author

@neoel one problem with namespacing is optimisation:

<!-- this will get re-evaluated whenever *any* namespaced property changes -->
<h1>Hello {$namespace.name}!</h1>
<p>I SAID HELLO {shoutyName}</p>

<script>
  export default {
    computed: {
      // and so will this
      shoutyName: ({ $namespace }) => $namespace.name.toUpperCase()
    }
  };
</script>

I don't know if that's a dealbreaker, but it's certainly something worth preserving if possible. Also, how would a component in the subtree set global properties, if this.store.set({ var: 42 }) only affected the local store? It would be a bit unfortunate to have to do this.store.parent.set(...) I think.

@jacwright you're 100% right. I prefer the second form

@dxlbnl
Copy link
Contributor

dxlbnl commented May 7, 2018

@Rich-Harris Hmm, hadn't thought of that.
But I do think making basically shadow copies of the global store doesn't compose nicely.

I agree that this.store.parent.set({}) isn't the way to go. what store is that parent really gonna be.

Idea:

How about named stores then? f.i: this.store.by('someCustomChildStore')

To outline some stuff:

  • Stores are flat structures, so they optimize easily.
  • Having multiple stores either makes them shadow copy the global store (basic js scoping rules should apply i guess)
  • Namespacing/nesting stores breaks the nice api for computed properties and nested store values inside html.

@funkybob
Copy link
Contributor

funkybob commented May 8, 2018

Following from how it works in Vue, you are able to create multiple modules (stores), each living within a namespace within the main store. They go further to allow getter access to (computed) views of these sub-namespaces.

What's most important to me [and it may be my Python background shining through] is to be able to namespace my values in the store to help avoid collisions...

@jacwright
Copy link
Contributor

jacwright commented May 8, 2018 via email

@Rich-Harris
Copy link
Member Author

Did you have an idea already or was that
still TBD?

If the child was explicit about which properties it had, there'd be no ambiguity. In other words, you would initialize all your properties:

export default {
  store: parent => new Store({
    foo: 1,
    bar: undefined // we don't have a value for bar yet, but we can 'claim' it for this store
  }, { parent })
};

Then, $set({ bar: 2 }) would affect the child; $set({ baz: 3 }) would affect the parent.

How big a need is there to allow parent and child stores to have properties of the same name? It feels to me like that would be something of an anti-pattern in the first place; less confusing to just use different names. We could semi-enforce that by warning or throwing an error if a store is created with a parent that has properties with the same names. But maybe I just haven't encountered situations where you would want same-named properties?

@jacwright
Copy link
Contributor

Here's a seed of an idea. Feel free to shoot down, poke holes, or expand.

{#if account.loggedIn}
<button on:click="account.signout()">Sign Out</button>
{/if}

<h2>All Projects</h2>
{#each dabble.projects as project}
<div on:click="dabble.set({ activeProject: project.id })">{project.name}</div>
{/each}

<h3>My Projects</h3>
{#each myProjects as project}
<div on:click="dabble.set({ activeProject: project.id })">{project.name}</div>
{/each}

<script>
import dabble from '../stores/dabble';
import account from '../stores/admins';

export default {
  computed: {
    myProjects: ({ dabble: { projects }, account: { userId } }) => {
      if (!userId) return [];
      return projects.filter(p => p.ownerId = userId);
    }
  },
  stores: { dabble, account },
}
</script>

I see good benefits here.

I have been enjoying the structure Svelte provides, making you import everything your component needs explicitly (actions, components, helpers, etc) rather than allowing subclassing etc. that would make the code in your component smaller, but also makes it harder to know where a component is getting some piece of data or method from. This makes store explicit too. To me, the great part about store is the sharing data/state at an app level, between components. It is not that you don't have to import the store.

This allows you to:

  • use any number of stores
  • subclass stores if you choose
  • use all your stores in computed properties
  • not have to use $-prefixed properties in your computed functions (a small thing, but still)
  • be explicit about where data is coming from and where it is being set
  • still allow the compiler to optimize all store access as it does now

We could provide this in addition to the existing model of inherited stores. We could also deprecate the existing model if we wanted to in favor of this.

@jacwright
Copy link
Contributor

Oh yeah, as far as property name conflicts, that only affects computed properties and can be dealt with like this:

  computed: {
    ownershipText: ({ dabble: { name: dabbleName }, account: { name: accountName } }) => {
      // lame example, but you get it
      return `${accountName} owns projects for ${dabbleName}`;
    }
  }

@Rich-Harris
Copy link
Member Author

It's an interesting idea, and I agree that explicitness is something to strive for, other things being equal. But it seems like this would make it very hard to create reusable components. Revisiting the example at the top...

<svg class="chart" bind:offsetWidth=$chart.width bind:offsetHeight=$chart.height>
  <!-- these automatically render nice axes based on
       the extents of the data, and the dimensions of the chart -->
  <Axis horizontal/>
  <Axis vertical/>

  <Scatterplot/>
  <VoronoiOverlay maxRadius={50} on:select="$set({ selected: event.point })"/>
</svg>

<script>
  import { Axis, Scatterplot, VoronoiOverlay, ChartStore } from 'svelte-charts';

  export default {
    store: parent => new ChartStore({
      // the ChartStore automatically creates a `chart` property and sets up
      // computations for scaling functions etc
    }, { parent }),
    components: { Axis, Scatterplot, VoronoiOverlay }
  };
</script>

...you can imagine that <Axis>, <Scatterplot> and <VoronoiOverlay> were imported from npm, and that the only requirement to use them was that some parent component had a store with a chart property (or maybe a collection of properties).

A component installed from npm can't realistically depend on a store in your app. I think that only works if components are responsible to passing their store to their children?

@jacwright
Copy link
Contributor

It feels like, to me, that reusable components should expect their data to be passed to them rather than pulling it from the store. I feel like store usage would be only within non-reusable app components, but that might just be my preference. Again, explicit feels better.

<Axis horizontal scale={chart.scales.horizontal}/>
<Axis vertical scale={chart.scales.vertical}/>

<Scatterplot scale={chart.scales} points={chart.dataPoints}/>

@jacwright
Copy link
Contributor

Rereading through this thread, I believe you are trying to make Store a useful utility where there can be multiple instances, one for each parent component of some sort. I have only ever treated Store as a global (or at least single-instance) provider of data.

A complex component such as the one you provided above could be done with a lot of data binding and events. But the templates could be simplified with a Store that is provided internally to that component and its children. I understand what you are getting at now. Sorry!

I'm down with this. It would be for small portions of your app. Not used like some Angular 1 controller hierarchy. In fact, should we limit the level to only allow 2 deep? Or let people get creative. 😄

@funkybob
Copy link
Contributor

funkybob commented May 9, 2018

I can see this thread is trying to cover several different ideas already.

  1. Providing "local" stores for sharing among related components

Vue offers provide/inject as one potential alternative solution to this.

  1. How to access the 'parent' store from a nested-store component.
    Also needs to be 3rd party friendly

  2. Providing stores namespaces
    This is the feature most important to me, currently.
    My original query was about having hierarchical data in a store, and still get all the reactivity goodness.
    Also (ideally) being able to access a subset of the graph for portions of my app.
    This is a pattern I've learned from a Vue app I'm working on, and would like to see something comparable in svelte.
    Not only does it allow components to keep their own namespace free of other code, but also makes testing easier, as the test harness can provide only the sub-namespace relevant to its components.

@PaulMaly
Copy link
Contributor

PaulMaly commented Jun 11, 2018

A few main thoughts about the store itself and multiple stores:

  1. Currently, multiple stores not working as described:

While it is possible for any subtree to have its own store... ...that makes it impossible for components in that subtree to access properties of the global store, which is often necessary.

Example

  1. Completely agree with:

It feels like, to me, that reusable components should expect their data to be passed to them rather than pulling it from the store.

Pulling data from the store is a side-effect. Reusable components should be pure as it possible.

  1. Components which implements its own Store basically would be autonomous and should be used as black-boxes. So, I think we don't need to ship more complexity to current store implementation. It's good now, except that first point - it's not working correctly.

  2. The main question of this issue - how to use top-level (or global) store in a subtree of components which have its own store. I believe that the good answer is we need to consider that subtree as an autonomous module. So, top-level store data could be just passed to the autonomous subtree through the props (maybe with two-bindings). The main component of subtree able to decide how to use that values.

Possible example:

App.html

<Filters />
<ProductsList filters=$filters />

<script>
  import store from './stores/MainStore.js';

  export default {
    store: () => store
  };
</script>

ProductsList.html

{#each $products as product (product.id) }
  <ProductItem {...product} />
{/each}

<script>
  import ProductItem from './components/ProductItem.html';
  import store from './stores/ProductStore.js';
 
  export default {
    store: () => store,
    onstate({ current: { filters } }) {
        this.store.set({ filters });
    },
    components: { ProductItem }
  };
</script>

In this example, App and Filters components use MainStore, ProductsList and all sub-components use ProductStore. If we need to have some data from MainStore inside whole subtree, we can just passed them to ProductStore manually.

But there are few exceptions:

  1. If we use slots or pass some components through props...
<ProductsList item=$ExternalItem />
<!-- OR -->
<ProductsList>
   <ExternalItem />
</ProductsList>

...in this case, that components should use store from their, top-level, context.

And the last thing, I believe we don't need to have direct access to top-level stores at all. We can use props and two-way binding, manually setup values to subtree store, fire events to the top to sync values, etc.

@funkybob
Copy link
Contributor

Just some idle thoughts as I skim this thread...

as mentioned before, I've found it useful [in other frameworks] to have namespaces within the top store... would it be possible to support such a feature here, where you denote sub-stores with a namespace, and pass on to a component its store prefix/namespac?

@btakita btakita mentioned this issue Jun 12, 2018
@PaulMaly
Copy link
Contributor

And small off-topic, sometimes when we use one global custom store with custom methods this store is growing faster than we want. To keep things simple we need to split it to different stores with own domain logic and methods.

One way to do that with the current implementation of Store is using aggregation:

ProductStore.js

export default class {	
	initializer() {
		this.set({ products: [] });
	}
	fetchProducts() {
	  ...
	}
}

Store.js

import aggregation from 'aggregation/es6';
import { Store } from 'svelte/store.js';

import MainStore from './stores/MainStore.js';
import ProductStore from './stores/ProductStore.js';

export default class extends aggregation(Store, MainStore, ProductStore) {}

main.js

import App from './App.html';
import Store from './Store.js';
const store = new Store();

export new App({
    target: document.body,
    store
});

But in this case, we'll get all the issues with naming, namespacing etc. inherent to mixins.

@PaulMaly
Copy link
Contributor

PaulMaly commented Jun 12, 2018

A complete example of my first comment: REPL

Looks simple and, seems, does not require any changes to the current Store. Most importantly, that it's not affecting any existing approaches. If someone wants to use single global store - welcome. Need to have autonomous subtree with its own store - sure, no problem.

Looks like, if we really need to have a component-level store when this subtree implementing some big piece of functionality or have a different domain logic. In this case, basically we don't need to have full global store, only some parts which can be passed manually.

Right now, only one problem with this example - SubChild use root store instead of its parent, that's why bar value unavailable there, but should be.

@yves-bourgeois
Copy link

yves-bourgeois commented Jul 12, 2018

Disclaimer: I have been working with Svelte for about a week now, and fell in love with it after a couple of hours.. I was amazed at how low the learning curve is, and how well it behaves.. So not an expert in regards to Svelte or Javascript..

The last example would, as far as I can see, only be a work-around to share data, and it would still be impossible to share custom functions between stores (and components).

We are trying to create a solution in which we would have one core component, which has store-functionalities such as user authorization, global filtering of data, etc. Then, depending on each customers' needs, we would like to dynamically load the necessary components, each having its own intricate store, and connect these stores dynamically, so that the different components can both communicate and call functions of our core component.

At the moment, we got as far as foreseeing a sub-store in our core-store -all custom stores that extend the Store-, which would be defined to the core-store in the oncreate of each component.

The idea: to be able to do something like this:

{ $coreProperty }
{ $substore.subProperty }    <-- this does not work, but we did manage to get the sub property value using:
{ $substore._state.subProperty }

The later is clearly far from ideal, but it does show that svelte is maintaining the state of the sub store correctly: changing values on either the core store or the sub store are being observed correctly.

However, what I was unable to do yet, is to actually call a function of the sub store.

This, to my feeling, is very similar to ideas that have been posted before. A store could be added to the parent store - living in its own variable-, which would make it impossible for both to clash, the parent does not really need to wonder whether its children would add stores by themselves.. If, at least, all child components use a unique name for the store variable inside the parent store.

Has there been any further progress on this?

kr,
Yves

@PaulMaly
Copy link
Contributor

@yves-bourgeois Hi!

The last example would, as far as I can see, only be a work-around to share data, and it would still be impossible to share custom functions between stores (and components).

Here we talking not about sharing functions between stores or components. To do that you can simply import that functions in all places you need it.

Then, depending on each customers' needs, we would like to dynamically load the necessary components, each having its own intricate store, and connect these stores dynamically, so that the different components can both communicate and call functions of our core component.

We already have core component (root) and single global store (root store). You able to call it in any level of the hierarchy:

// nested-sub-nested component

const globalStoreData = this.root.store.get();

this.root.store.someMethod();

This will work even if sub-hierarchy will re-declare its own store, but I believe it's too complicated and will made whole your app is very coupling.

At the moment, we got as far as foreseeing a sub-store in our core-store -all custom stores that extend the Store-, which would be defined to the core-store in the oncreate of each component.

I think sub-stores should be used only in fully isolated sub-hierarchies with minimal dependensies with top-level things. If you just need to split you global functionality to different classes you able to use aggregation like in example above.

@yves-bourgeois
Copy link

@PaulMaly aggregation might not work if the functionality depends on state..\

+1 for named stores !

@btakita
Copy link
Contributor

btakita commented Aug 18, 2018

A situation where I found multiple stores to be useful is injecting a private store into a component that would otherwise act on global store data. Right now the component accepts a store prop that can override the global store data.

Using a store prop allows for a store to be injected from outside the component or instantiated within the component definition.

I've had success with this approach, with an annoyance. The injected store may change & it's events need to be managed (subscribed to & unsubscribed to). Svelte could help out by handling the event binding management.

I get the sense that store props solves the issue of having a reference to the global store while allowing n additional stores to be utilized by the component.

@coussej
Copy link

coussej commented Sep 27, 2018

May I ask if there is any update on where this is headed?

The reason I'm interested: we are building an application that is part product and part customer specific. We need the customer modules to have their own store (with custom logic) while having access to the main store (with user information, link to the backend, etc.) The ability to nest or compose stores would thus be great.

@clitetailor
Copy link

IMO, i think it's not too hard. For 3rd-party component, we can explicitly specify the stores for them:

<!-- Explicitly specify the providers for 3rd-party component -->
<svelte:provider inherit="false" store:theme="themeStore">
  <Sidebar/>
</svelte:provider>

<!-- The stores are passed down implicitly to other components -->
<div class="c-layout">
  <MyComponent/>
</div>

<script>
  import { Sidebar } from 'svelte-material'
  import { themeStore } from './stores/theme'

  export default {
    stores: (stores) => ({
      ...stores,
      theme: themeStore
    }),

    components: {
      Sidebar
    }
  }
</script>

For composing stores, i think we can change the api a bit. For example:

export const budgetStore = new Store({
  stores: {
    balanceStore,
    transactHistoryStore
  },

  methods: {
    increaseBalance(amount) {
      const { balanceStore, transactHistoryStore } = this.get()

      balanceStore.increase(amount)
      transactHistoryStore.addAction({
        type: 'INCREASE',
        amount
      })
    }
  },

  compute: {
    balance: ({ balanceStore }) => balanceStore.value
  }
})

So that we can call store's method in our component:

<div>Balance {budgetStore.balance}</div>

<button on:click="budgetStore.increaseBalance(amount)">
  Add
</button>

<script>
  import { budgetStore } from './stores/budget-store'

  export default {
    stores: {
      budgetStore
    }
  }
</script>

And for stores reuse, we can have something like store.clone(). When it's called, it will also clone sub-stores.

However, it's only my ideas. How do you think?

@Rich-Harris
Copy link
Member Author

It's probably time to close this issue, as this problem is solved in v3 with reactive stores. The tl;dr is that a 'store' now contains a single value, rather than all your app state, which makes it easy to combine and compose them and have stores at different levels of your app.

@bradphelan
Copy link

I think you can have your cake and eat it too. Start with a single store with all your global state and then split off views from that main store. As a proof of concept I have written a tool called subStore.

Examples and links to repl can be found here

https://github.com/bradphelan/immer.loves.svelte

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

10 participants