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

Support for syntax for binding to arbitrary reactive store #4079

Closed
Quantumplation opened this issue Dec 9, 2019 · 34 comments
Closed

Support for syntax for binding to arbitrary reactive store #4079

Quantumplation opened this issue Dec 9, 2019 · 34 comments

Comments

@Quantumplation
Copy link

Quantumplation commented Dec 9, 2019

(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:

function createMapStore(initial) {
  const backingStore = writable(initial);
  const { subscribe, update } = backingStore;
  const set = (key, value) => update(m => Object.assign({}, m, {[key]: value}));
  return {
    subscribe,
    set,
    remove: (key) => set(key, undefined),
    keys: derived(backingStore, bs => Object.keys(bs)),
    values: derived(backingStore, bs => Object.values(bs)),
    entries: derived(backingStore, bs => Object.entries(bs)),
  }
}

In theory this would allow me to do things like

{#each $store.values as value}

However, this doesn't appear to work. I get the following error:

bundle.js:1505 Uncaught TypeError: Cannot read property 'length' of undefined
    at create_fragment$4 (bundle.js:1505)
    at init (bundle.js:390)
    at new Home (bundle.js:1577)
    at Array.create_default_slot_3 (bundle.js:2548)
    at create_slot (bundle.js:48)
    at create_if_block (bundle.js:1080)
    at create_fragment$3 (bundle.js:1128)
    at init (bundle.js:390)
    at new TabPanel (bundle.js:1239)
    at Array.create_default_slot$2 (bundle.js:2674)

bundle.js:89 Uncaught (in promise) TypeError: Cannot read property 'removeAttribute' of undefined
    at attr (bundle.js:89)
    at attr_dev (bundle.js:455)
    at update (bundle.js:856)
    at updateProfile (<anonymous>:49:7)
    at Object.block.p (<anonymous>:261:9)
    at update (bundle.js:188)
    at flush (bundle.js:162)

I can work around this by doing:

<script>
import { store } from './stores.js';
$: values = store.values;
</script>

{#each $values as item}

Describe the solution you'd like
Ideally, I'd simply be able to do:

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

{#each store.values as item}

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.

@Conduitry
Copy link
Member

#each store.values as item} isn't really an option, because store.values isn't an array - it's a store containing an array.

$store.values means the .values key in the object contained in the store store, which is not the situation you have.

If you expect store.values itself to change (and not just the value in it), then something like the reactive declaration $: values = store.values; is what is recommended. If it's not going to change, you can just do const { values } = store; and then use $values.

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,

@Quantumplation
Copy link
Author

@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:

{#each $(stores.values) as item} <!-- might not play well with jQuery / $ selectors -->
// or
{#each $stores.$values as item}
// or
{#each stores.$values as item}

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

<script>
import { storeA, storeB, storeC } from './stores';
const { aKeys } = storeA;
const { bKeys } = storeB;
const { cKeys } = storeC;
$: aCount = $aKeys.length;
$: bCount = $bKeys.length;
$: cCount = $cKeys.length;
</script>
<div># of As: {aCount}</div>
<div># of Bs: {bCount}</div>
<div># of Cs: {cCount}</div>

as opposed to

<script>
import { aStore, bStore, cStore } from './stores.js';
</script>
<!-- Each syntax shown -->
<div># of As: {$(aStore.keys).length}</div>
<div># of Bs: {$bStore.$keys.length}</div>
<div># of Cs: {cStore.$keys.length}</div>

@freedmand
Copy link

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 $ reactive. This means in {#each $store.values as value} only $store is reactive, and $store returns your JavaScript object (initial) which doesn't have a values property.

You can fix this by having a derived store off your backing store that returns an object with keys, values, and entries properties. I've quickly rigged an example of this working here: https://svelte.dev/repl/ccbc94cb1b4c493a9cf8f117badaeb31?version=3.16.7

Shameless plug: I've created a package called Svue to make complex store patterns more tractable with Svelte and play nicely with the $ reactive syntax. It's admittedly early stages and could be cleaned up a bit with respect to nested properties, but here's an example of a structure like what you're doing above using Svue: https://svelte.dev/repl/2dd2ccc8ebd74e97a475db0b0da244d9?version=3

@skflowne
Copy link

skflowne commented May 7, 2020

I've worked around this by creating a derived store that's basically just creating a new object with the values of the other stores.

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

{#if $(principleCollection.loading)}

This would avoid having to create a whole derived store that's basically just repeating three times every variable name.
Not sure if there's a better way that allows not to have to use destructuring because, as pointed out previously, if I add a tagCollection then I can't just do const { loading } = principleCollection anymore or I have to repeat and rename everything by doing $: principlesLoading = principleCollection.loading and $: tagsLoading = tagCollection.loading which is definitely not what I want.

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 Component.ts file of the compiler.
Any pointers on what needs to be changed to accomplish this ?

@Quantumplation
Copy link
Author

@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.

@skflowne
Copy link

Can someone explain why it's not working this way right now ?
Is there some major technical issue related to extracting nested variables in template expressions ?
Or is it just about agreeing on syntax ?

@brunnerh
Copy link
Member

brunnerh commented Aug 19, 2020

It would be great if there were some syntax for directly subscribing to stores in properties of objects.
So ideally just regularObject.$childStore and $store.$childStore or if that somehow is not an option maybe the $ can be nested via parentheses like $(regularObject.childStore) and $($store.childStore).

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 🙁 - Stores must be declared at the top level of the component (this may change in a future version of Svelte))

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.

@pushkine
Copy link
Contributor

pushkine commented Sep 7, 2020

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
https://svelte.dev/repl/d2c8c1697c0f4ac3b248889ec329f512?version=3.24.1

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!"

@cie
Copy link

cie commented Dec 4, 2020

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.)

@wagnerflo
Copy link

+1. Would really like syntax support for this, too!

@lgrahl
Copy link

lgrahl commented Feb 16, 2021

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.

$ very much behaves like an operator in my opinion, so perhaps we should define some operator precedence, relative to the existing ones, to resolve this issue in a satisfying manner. Grouping (via braces) could then be applied naturally.

@tanepiper
Copy link

tanepiper commented Feb 21, 2021

+1 - I'm adding a new API to svelte-formula called beaker that allows the creation of form groups.

When creating a group object (e.g. const contacts = beaker()) the contacts variable contains an action and some stores.

In the temple the cleanest way to use this would be:

<div use:contacts.group>
  {# each $contacts.formValues as contact, i}
  {/each}
</div>

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 $ as a variable here)

<div use:contacts.group>
  {# each $(contacts.formValues) as contact, i}
  {/each}
</div>

@pngwn pngwn changed the title Automatic subscriptions to nested stores Automatic subscriptions to nested stores / contextual stores May 30, 2021
@pngwn
Copy link
Member

pngwn commented May 30, 2021

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.

Examples

An 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 #each block (extension of above)

<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}

Comments

I 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 (foo.$bar)

We never actually did this and the conversation probably isn't very useful but I never want to do this again.
https://discord.com/channels/457912077277855764/571775594002513921/848691193046892594

Discord conversation about stores in stores

https://discord.com/channels/457912077277855764/457912077277855766/683966382790148117

Discord conversation about maybe not needing this at all

Conversation starts here

@tanhauhau mentioned that the #with syntax could actually be one way to address this. The #with syntax has probably been superseded by the @const proposal at this stage (although that is TBD). But they would both address some of these use-cases in one way or the other.

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 @const at the top level (outside of template a sub-scope: each, etc).


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.

@btakita
Copy link
Contributor

btakita commented Jun 18, 2021

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 value is a proxy for the reactive store value $value$. On the surface, it seems like the label proxy is a different use case (being in the <script>), but it seems like a use case which could be a factor in the design of contextual store proxies or even contextual reactive variables.

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}

@stale
Copy link

stale bot commented Dec 24, 2021

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.

@stale stale bot added the stale-bot label Dec 24, 2021
@rmunn
Copy link
Contributor

rmunn commented Dec 24, 2021

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.

@stale stale bot removed the stale-bot label Dec 24, 2021
@LokiMidgard
Copy link

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)

Workaround that should work for arrays and nested proeprtys

import { get, readable, Readable, writable } from "svelte/store";




export type NoStore<T> = T extends Readable<infer T2>
    ? NoStoreParameter<T2>
    : NoStoreParameter<T>;


type NoStoreParameter<T> = {
    [Property in keyof T]: T[Property] extends Readable<infer Args> ? NoStore<Args> : NoStore<T[Property]>
}


export function flatStore<T>(source: T): Readable<NoStore<T>> {

    return readable({} as NoStore<T>, function start(set) {
        let destroyCallback: (() => void)[] = [];

        const updated: () => void = () => {
            destroyCallback.forEach(x => x());
            const newDestroyCallback: (() => void)[] = [];
            const newValue = mapStoreInternal(source, { update: updated, onDestroy: newDestroyCallback })
            destroyCallback = newDestroyCallback;
            set(newValue)
        };
        const startValue = mapStoreInternal(source, { update: updated, onDestroy: destroyCallback })
        console.log(startValue);
        set(startValue);
        return function stop() {
            destroyCallback.forEach(x => x());
        }
    });

}
function mapStoreInternal<T>(source: T, callbacks?: { update: () => void, onDestroy: (() => void)[] }): NoStore<T> {

    if (isStore(source)) {
        const value = get(source);
        if (callbacks) {

            const unsubscribe = source.subscribe(x => {
                if (value !== x) {
                    callbacks.update();
                }
            })
            callbacks.onDestroy.push(unsubscribe);
        }
        return mapStoreInternal(value, callbacks) as NoStore<T>;

    } else if (Array.isArray(source)) {
        const result: any[] = []
        for (let index = 0; index < source.length; index++) {
            const element = source[index];
            result.push(mapStoreInternal(element, callbacks));
        }
        return result as any;
    } else if (typeof source === "object") {
        const result: any = {}
        for (const key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                const element = source[key];
                result[key] = mapStoreInternal(element, callbacks);
            }
        }
        return result;
    }
    else {
        // only stuff like string and bigint
        return source as any;
    }
}


function isStore(value: any): value is Readable<any> {
    if (value)
        return typeof value.subscribe == "function";
    return false;
}

@iacore
Copy link

iacore commented Jan 5, 2022

@iacore
Copy link

iacore commented Jan 5, 2022

@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".

@iacore
Copy link

iacore commented Jan 5, 2022

So, about making $ as an operator. The dot (.) always has the highest precedence, so the semantic of $foo.bar would change from ($foo).bar to $(foo.bar).

This change will make the use of $ in line with ! and other unary operators, but will break a lot of (speculation) code. And that can be solved by first making parenthesis recommended, then mandatory, then change the default behavior (like how new features introduced in D). We can also provide auto refactor tools for this change.

@Quantumplation Quantumplation changed the title Automatic subscriptions to nested stores / contextual stores Support for syntax for binding to arbitrary reactive store Jan 5, 2022
@Quantumplation
Copy link
Author

@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".

Done!

@lgrahl
Copy link

lgrahl commented Jan 7, 2022

So, about making $ as an operator. The dot (.) always has the highest precedence, so the semantic of $foo.bar would change from ($foo).bar to $(foo.bar).

This change will make the use of $ in line with ! and other unary operators, but will break a lot of (speculation) code. And that can be solved by first making parenthesis recommended, then mandatory, then change the default behavior (like how new features introduced in D). We can also provide auto refactor tools for this change.

An in hindsight thought (and apologies for the slight slidetrack): At first, everyone was confused on why Rust put the .await keyword at the very end. I think we can learn from that. 🙂

@gustavopch
Copy link

Just want to let my use case here. I like to use namespace imports so I have a single import * as Lib from '$lib' and then I can access whatever I need from there: Lib.Firebase.User.get(id), Lib.Image.preload(url), <Lib.UI.Button />, Lib.Store.currentUser (this one is a store), and so on. It helps me immediately identify from where a function/variable comes and whether it's declared in the current module or somewhere else. It also completely prevents me from wasting "brain cycles" with name collisions that would sometimes happen when importing things with the same name from different modules. I'm explaining here just for context, but, of course, it's a personal preference.

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 $() makes a lot of sense to me and would make my code cleaner. It's one of the few cases where Svelte doesn't yet make my code as clean as possible. I'm hoping $() gets added soon.

@jquesada2016
Copy link

$() syntax is the first thing I tried when I ran into this, but got an error, and landed here. This syntax would be great.

@techniq
Copy link

techniq commented Mar 13, 2022

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.

@WHenderson
Copy link

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.

@WHenderson
Copy link

$() syntax is the first thing I tried when I ran into this, but got an error, and landed here. This syntax would be great.

Same for me. I wanted to ergonomically reference a store contained in an object and intuitively reached for the $(...) syntax.

+1 for supporting $(some expression resulting in a store) as the syntax for binding to an arbitrary store expression (alongside the existing syntax for simple stores).

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).
At the moment I am forced to create a long ugly list of local variable references in each component and use sub-components for otherwise trivial tasks like iterating over loops with inner stores such as described in #2016

@mquandalle
Copy link

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)

@btakita
Copy link
Contributor

btakita commented Sep 23, 2022

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 ctx Map to hold lazily loaded stores for years now. It's not slick syntactic sugar, but it handles the concurrent (e.g. server-side) state outside of the component tree.

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

@emensch
Copy link

emensch commented Jun 22, 2023

Any more thinking on this? Would personally love to see a $() (or similar) syntax - it's the only pain point I've had using svelte so far.

@DrStrangeloovee
Copy link

With yesterdays preview of Svelte 5. Will this simplify or even solve this issue?

@brunnerh
Copy link
Member

I think it should. Everything reactive can be modelled via $state anywhere.

@Rich-Harris
Copy link
Member

I'm going to close this — we're not going to add new features around stores, since Svelte 5's $state(...) effectively supersedes them and solves the problems described in this issue

@samal-rasmussen
Copy link

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 StoreSubscribe component from @techniq working with svelte 5 syntax.

<script lang="ts" generics="T">
	import { type Snippet } from 'svelte';
	import type { Readable } from 'svelte/store';

	const { store, content }: { store: Readable<T[]>; content: Snippet<[T[]]> } = $props();
	let subscription_value: T[] = $derived($store);
</script>

{@render content(subscription_value)}
<StoreSubscribe store={store_you_want_to_subscribe}>
    {#snippet content(value)}
        ....
    {/snippet}
</StoreSubscribe>

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

No branches or pull requests