-
-
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
Support for syntax for binding to arbitrary reactive store #4079
Comments
If you expect As the docs indicate, autosubscription to stores only works with top-level variables. There are some situations where it would be nice to be able to do more than this - but one of the things in the way of that is there not being a nice syntax for it, and I don't think this issue suggests one. Closing, |
@Conduitry instead of closing immediately, could we discuss some syntaxes that might work? I'm hardly an expert, so I'm not sure I can propose a syntax, but I can try to get the ball rolling:
would all be in the realm of possibility for me. Certainly nicer than requiring a const destructuring each time. Proposed workarounds get particularly awkward for multiple stores
as opposed to
|
I had this problem and figured out how to make something like this work with the compiler quirks. In essence, the compiler will only make whatever is immediately attached to the You can fix this by having a derived store off your backing store that returns an object with Shameless plug: I've created a package called Svue to make complex store patterns more tractable with Svelte and play nicely with the |
I've worked around this by creating a I created a class to communicate with a Firestore collection that looks like this import firebase from "../firebase"
import { writable, readable, derived } from "svelte/store"
export default class firestoreCollection {
constructor(name) {
this.name = name
this.ref = firebase.firestore().collection(name)
this.loading = writable(false)
this.loadingError = writable(null)
this.dict = readable([], (set) => {
console.log("subscribing to", this.name)
this.loading.update((p) => true)
this.ref.onSnapshot(
(s) => {
this.loading.update((p) => false)
const entities = {}
s.forEach((doc) => {
entities[doc.id] = { id: doc.id, ...doc.data() }
})
this.loadingError.update((p) => null)
console.log("onSnapshot", this.name, "entities:", entities)
set(entities)
},
(e) => {
console.error("failed to load entities", this.name, e)
this.loading.update((p) => false)
this.loadingError.update((p) => e)
}
)
})
this.entities = derived(this.dict, ($dict) => {
return $dict ? Object.values($dict) : []
})
this.adding = writable(false)
this.addError = writable(null)
this.updating = writable(false)
this.updateError = writable(null)
this.store = derived(
[
this.loading,
this.loadingError,
this.adding,
this.addError,
this.updating,
this.updateError,
this.entities,
],
([$loading, $loadingError, $adding, $addError, $updating, $updateError, $entities]) => {
return {
loading: $loading,
loadingError: $loadingError,
adding: $adding,
addError: $addError,
updating: $updating,
updateError: $updateError,
entities: $entities,
}
}
)
}
async add(newEntity) {
try {
this.adding.update((p) => true)
await this.ref.add(newEntity)
this.adding.update((p) => false)
this.addError.update((p) => null)
} catch (e) {
console.error("add failed", this.name, newEntity, e)
this.addError.update((p) => e)
}
}
async update({ id, ...updatedEntity }) {
try {
this.updating.update((p) => id)
await this.ref.doc(id).set(updatedEntity)
this.updating.update((p) => false)
this.updateError.update((p) => null)
} catch (e) {
console.error("failed to update", this.name, id, e)
this.updating.update((p) => false)
this.updateError.update((p) => ({ id, error: e }))
}
}
} Then I'd do import firestoreCollection from "../firebase/firestoreCollection"
const principleCollection = new firestoreCollection("principles")
export default principleCollection And import this into my component import principleCollection from "./store/principles";
$: principles = principleCollection.store; {#if $principles.loading}
<p>Loading principles...</p>
{:else}
{#if $principles.loadingError}
<p class="text-red-500">{$principles.loadingError.message}</p>
{:else if $principles.entities && $principles.entities.length}
<div class="flex flex-row flex-wrap">
{#each $principles.entities as principle (principle.id)}
<Principle {principle} on:save={savePrinciple(principle.id)} />
{/each}
</div>
{:else}
<p>No principles yet</p>
{/if}
<button
on:click={e => principleCollection.add({ content: 'My new principle' })}>
Add new
</button>
{/if} While this works fine, I would have preferred to be able to directly access the instance stores like so
This would avoid having to create a whole derived store that's basically just repeating three times every variable name. I'd like to see if can implement this, I've started to look at the code for Svelte. I've noticed areas of interest seem to be in the |
@skflowne yea, what you're describing is essentially the workaround I mentioned in my initial comment, I could never find a cleaner way to do it either. |
Can someone explain why it's not working this way right now ? |
It would be great if there were some syntax for directly subscribing to stores in properties of objects. Currently the issue often comes up with for-each blocks because for singular instances one can just pull the property to the top level and then use that (it is still not intuitive). So for example: <script>
export let model;
$: isEnabled = model.isEnabled;
</script>
<button disabled={$isEnabled == false}>{model.label}</button> Thus, another workaround is to create a new top level scope for each item by wrapping the content of a for-each block in a new component. That is hardly ideal and i have been thinking that being able to add code to the for-each scope would be a useful capability in itself. (One can already destructure the loop variable but using a store obtained that way currently throws an error 🙁 - Example with fantasy syntax: {#each buttonModels as buttonModel {
// Code block with access to for-each item scope.
const isEnabled = buttonModel.isEnabled;
}}
<button disabled={$isEnabled == false}>{model.label}</button>
{/each} This could also be used for getting some item-level data on the fly without the need to map over the source array or having overly long expressions in attribute bindings and slots. |
Here's a proxy store I wrote to derive the value of a store nested within other stores, it plays nice with typescript and can go infinitely deep type Cleanup = () => void;
type Unsubscriber = () => void;
type CleanupSubscriber<T> = (value: T) => Cleanup | void;
type p<l, r> = (v: l) => Readable<r>;
export function proxy<A, B>(store: Readable<A>, ...arr: [p<A, B>]): Readable<B>;
export function proxy<A, B, C>(store: Readable<A>, ...arr: [p<A, B>, p<B, C>]): Readable<C>;
export function proxy<A, B, C, D>(store: Readable<A>, ...arr: [p<A, B>, p<B, C>, p<C, D>]): Readable<D>;
export function proxy<A, B, C, D, E>(store: Readable<A>, ...arr: [p<A, B>, p<B, C>, p<C, D>, p<D, E>]): Readable<E>;
export function proxy(store: Readable<any>, ...arr: p<any, any>[]) {
const max = arr.length - 1;
return readable(null, (set) => {
const l = (i: number) => (p) => {
const q = arr[i](p);
if (!q) set(null);
else return i === max ? q.subscribe(set) : subscribe_cleanup(q, l(i + 1));
};
return subscribe_cleanup(store, l(0));
});
}
function subscribe_cleanup<T>(store: Readable<T>, run: CleanupSubscriber<T>): Unsubscriber {
let cleanup = noop;
const unsub = store.subscribe((v) => {
cleanup();
cleanup = run(v) || noop;
});
return () => {
cleanup();
unsub();
};
} Simply supply your store followed by however many functions are needed to derive from the value of each nested store const deepest = readable("success!");
const deeper = readable({ deepest });
const deep = readable({ deeper });
const store = readable({ deep });
const res = proxy(
store,
($store) => $store.deep,
($deep) => $deep.deeper,
($deeper) => $deeper.deepest
);
console.log($res); // "success!" |
A slightly different use case. I often use functions that return stores. Now I do something like this $: PRODUCT = watchProduct(product_id)
$: product = $PRODUCT
<h1>{product.title}</h1>
{product.description} or $: product$ = watchProduct(product_id)
<h1>{$product$.title}</h1>
{$product$.description} but I'd prefer instead a less noisy $: product = $(watchProduct(product_id))
<h1>{product.title}</h1>
{product.description} (Actually maybe this could be done with a Babel Macro.) |
+1. Would really like syntax support for this, too! |
This issue even occurs when importing via namespaces, so I think it's quite important to resolve it. import * as stores from './stores';
...
$stores.foo The issues I see with the the presented workarounds is that all of them wrap multiple stores into one, resulting in a multiplied performance impact on evaluation.
|
+1 - I'm adding a new API to svelte-formula called When creating a group object (e.g. In the temple the cleanest way to use this would be:
But like other examples above you need to create a reference earlier to it in another variable before using. I was wondering if somehow templates could handle an expression like this at least? (currently doesn't work as it treats
|
This has come up again in #6373. This comment has some more commentary on this feature and potentially expands it somewhat. I have been trawling through GitHub and discord to see what has been said about this issue in the past. I will try to document what I could find as well as capturing a few core cases that this feature would need to cover. All example will be pseudocode to communicate the essence of the problem and are not indicative of any possible solution/ eventual syntax. ExamplesAn object property containing a store: <script>
import { writable } from 'svelte/store';
const my_store = {
one: writable(1),
two: writable(2)
};
</script>
{my_store.$one} - {my_store.$two} A computed object property containing a store (raised by @Rich-Harris in this comment): <script>
import { writable } from 'svelte/store';
const my_store = {
one: writable(1)
};
const my_prop = 'one';
</script>
{my_store[`$${my_prop}`]} An array of stores (very similar to the above if not identical): <script>
import { writable } from 'svelte/store';
const my_store = [ writable(1) ];
</script>
<!-- this is a fucking monstrosity -->
{my_store[$0]} Iterating an array of stores in an <script>
import { writable } from 'svelte/store';
const todos = [{
description: 'my todo',
done: false
}];
</script>
{#each todos as todo}
<div>
<input type=checkbox bind:checked={$todo.done}>
{$todo.description}
</div>
{/each} All of the above but also in a store (recursive stores?) I'm not entirely certain what the use-case for this is, but if we are considering adding some runtime to support dynamically computed contextual stores, we can probably support this too. <script>
import { writable } from 'svelte/store';
const my_store = writable({
one: writable(1),
two: writable(2)
});
</script>
{$my_store.$one} - {$my_store.$two} CommentsI haven't been able to find much that is useful or relevant but I'm going to dump some fragments of conversations and discord links here so we stand a chance of finding them in the future. Discord conversations about reserving contextual store accessor syntax ( We never actually did this and the conversation probably isn't very useful but I never want to do this again. Discord conversation about stores in stores https://discord.com/channels/457912077277855764/457912077277855766/683966382790148117 Discord conversation about maybe not needing this at all @tanhauhau mentioned that the While this could technically work, I don't really think it address the core issues: <script>
import { writable } from 'svelte/store';
const my_store = {
one: writable(1),
two: writable(2)
};
</script>
{@const one = my_store.one }
{@const two = my_store.two }
{$one} - {$two} This is almost exactly as much code as the current workaround (deconstructing the object in a reactive declaration) and doesn't address any of the other use-cases (stores in arrays, stores in stores, computed property names containing stores). There has also been some discussion about banning the use of I think this captures the commonly use-cases and a few of the more interesting conversations that have happened outside of this issue. What I found plenty of in my search, was requests for this features and possible use-cases. If they would be helpful I could potentially dump them here as well. |
In #6373 (comment), an idea using labels came to me. Apologies if somebody already came up with this idea. <script lang=ts>
import { value$ } from './value$'
export value = $value$
value: $value$
</script>
<input bind:value> Where the reactive variable Within the template area, this could be extended to: {#each my_store_objs as my_store_obj}
{val_w_suffix: my_store_obj.$val_w_suffix$}
{val_wo_suffix: my_store_obj.$val_wo_suffix}
{$: sum = val_w_suffix + val_wo_suffix}
{alt_sum: val_w_suffix + val_wo_suffix}
<input type=number bind:value={val_w_suffix}> + <input type=number bind:value={val_wo_suffix}> = {sum} or {alt_sum}
{/each} |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Further activity to prevent stale bot from closing this. Also, does the stale bot contribute any value commensurate with the extra effort it takes to prevent it from closing valuable issues with lots of discussion? If it does not (and IMHO it does not), I suggest we stop using it. |
It would be awsom if this would work out of the box. I had an store of an array of objects with store propertys... I tried some of the workarounds here, but the arrays made some problems. I worked around this by creating a store that subscribs to every update of any store reachable through object propertys, arrays etc. And used some Typescript sorcery to turn Readable propertys to a "normal" type. (I must say TypeScript's type system is awsome)
|
@Quantumplation @Conduitry Can you change the title of this issue, since its scope is larger than the original problem? Change to something like "Support for syntax for binding to arbitrary reactive store". |
So, about making This change will make the use of |
Done! |
An in hindsight thought (and apologies for the slight slidetrack): At first, everyone was confused on why Rust put the |
Just want to let my use case here. I like to use namespace imports so I have a single Ideally, this is what I'd like to do: <script>
import * as Lib from '$lib'
</script>
{$(Lib.Store.currentUser).email} As it's not yet possible, the best I can do while preserving the namespace is this: <script>
import * as Lib from '$lib'
const Lib_Store_currentUser = Lib.Store.currentUser
</script>
{$Lib_Store_currentUser.email} The |
|
A related issue I've ran into is subscribing to a store value passed as a slot prop. A solution/workaround I created is a simple StoreSubscribe wrapper component, such as: <Parent let:someStore>
<StoreSubscribe value={someStore} let:value>
<Child {value} />
</StoreSubscribe>
</Parent> Looking at it, it might make more sense if the props were <StoreSubscribe store={someStore} let:value> or <StoreSubscribe store={someStore} let:$store> Anyways, thought I'd share in case it helps anyone. |
This is a solution I've used in the past, but its a bit clunky and doesn't work for two way binding. |
Same for me. I wanted to ergonomically reference a store contained in an object and intuitively reached for the +1 for supporting Having this syntax would be such a quality of life improvement for my projects. I store state in immutable trees (@crikey/stores-immer) and generate reactive stores on the fly using selectors (@crikey/stores-selectable). |
It seems like SolidJS signals has a nice API that works for composing stores, which permits “derived stores of derived stores” like in the pseudo-code examples of #4079 (comment) |
My main complaint over solid-js signals is that they have global state & are primarily designed to run inside a component tree. Reactive domain data is not really supported. It's possible but kludgy to use a solid-js signal in middleware & components. However nanostores (a close fork of svelte stores) & svelte stores are designed to be executed outside of a component tree. Also, with a context, svelte stores & nanostores can be run concurrently. I have been using a concurrency-friendly pattern of injecting a import { be_, ctx_ } from '@ctx-core/object'
import { writable_ } from '@ctx-core/svelte'
const count__ = be_(()=>writable_(0))
const ctx = ctx_()
my_writable__(ctx).$ = 1 wrt solid-js & nanostores import { be_, ctx_ } from '@ctx-core/object'
import { atom_ } from '@ctx-core/nanostores'
import { useMemo } from '@ctx-core/solid-nanostores'
const count__ = be_(()=>atom_(0))
const ctx = ctx_()
function MyComponent() {
const count_ = useMemo(count__(ctx))
return [
<div>{count_()}</div>,
<button onClick={()=>count__increment(ctx)}>Increment</button>
]
}
// Demonstrates function decomposition
function count__increment(ctx:Ctx) {
count__(ctx).$ = count__(ctx).$ + 1
} If something like solid signals supports being run outside of the component tree & not reliant on global state (i.e. concurrency friendly), then we can slim things down even more. @ryansolid has practical reasons for using global state for the needs of solid-js but I think there's a case for supporting general purpose domain reactive state not being run inside a component tree. import { createSignal } from 'new-library'
const [count_, count__set] = createSignal(0)
console.info(count_()) // 0
count__set(1)
console.info(count_()) // 1 |
Any more thinking on this? Would personally love to see a |
With yesterdays preview of Svelte 5. Will this simplify or even solve this issue? |
I think it should. Everything reactive can be modelled via |
I'm going to close this — we're not going to add new features around stores, since Svelte 5's |
I have an rxjs observable that I want to subscribe to inside an #each block if the user presses a button on that item. Would love to hear some ideas how I can solve that with runes... In the mean time I got the
|
(Not sure if this is a feature request or a bug...)
Is your feature request related to a problem? Please describe.
I've implemented a custom store that's essentially:
In theory this would allow me to do things like
However, this doesn't appear to work. I get the following error:
I can work around this by doing:
Describe the solution you'd like
Ideally, I'd simply be able to do:
Describe alternatives you've considered
See the
$: values = store.values;
approach above.How important is this feature to you?
It's not super important, but so far Svelte has had excellent emphasis on ergonomics, so it's a bit of a shame that this doesn't work.
The text was updated successfully, but these errors were encountered: