-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Comments
I like the idea, but thinking about multiple stores, I think there should be another pattern possible.
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:
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. |
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. |
@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 @jacwright you're 100% right. I prefer the second form |
@Rich-Harris Hmm, hadn't thought of that. 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: To outline some stuff:
|
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... |
If we didn’t do `this.store.parent.set()`, what *is* the way you would set
data on the parent store? You wouldn’t make `this.store.set()` guess which
properties are local vs global. Did you have an idea already or was that
still TBD?
Should we have a standard way to access the parent? So that you can call
methods on subclassed parent stores.
Additionally, while we can’t optimize `$namspace.name` with the compiler,
we *could* optimize `$parent.name` by making `parent` a special case with
the compiler. This makes it clearer where data is coming from, allows
`$parent.set()` and other custom methods a subclassed store might have, and
still have optimized evaluations. But you can’t make computed properties
with `({ $parent.name }) =>` so... this brainstorm won’t work.
In my current app, while store is available via `this.store`, I only use
that feature for the `$` shortcut it provides in templates and computed
properties. For everything else I import my sunclassed store directly and
call the methods or get data from it that way.
|
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, 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? |
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:
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. |
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}`;
}
} |
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 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? |
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}/> |
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. 😄 |
I can see this thread is trying to cover several different ideas already.
Vue offers provide/inject as one potential alternative solution to this.
|
A few main thoughts about the store itself and multiple stores:
Pulling data from the store is a side-effect. Reusable components should be pure as it possible.
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, But there are few exceptions:
<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. |
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? |
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. |
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 - |
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:
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-bourgeois Hi!
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.
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.
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. |
@PaulMaly aggregation might not work if the functionality depends on state..\ +1 for named stores ! |
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. |
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. |
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 However, it's only my ideas. How do you think? |
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. |
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 |
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: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:
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:
Store
? Do we need to be able to specify the class of the child store? (Probably)$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:The text was updated successfully, but these errors were encountered: