sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.43k stars 4.11k forks source link

Support for syntax for binding to arbitrary reactive store #4079

Closed Quantumplation closed 5 months ago

Quantumplation commented 4 years ago

(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 commented 4 years ago

#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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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

lgrahl commented 3 years ago

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 commented 3 years ago

+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 commented 3 years ago

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 commented 3 years ago

In https://github.com/sveltejs/svelte/issues/6373#issuecomment-863801628, 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[bot] commented 2 years ago

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.

rmunn commented 2 years ago

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.

LokiMidgard commented 2 years ago

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** ```ts import { get, readable, Readable, writable } from "svelte/store"; export type NoStore = T extends Readable ? NoStoreParameter : NoStoreParameter; type NoStoreParameter = { [Property in keyof T]: T[Property] extends Readable ? NoStore : NoStore } export function flatStore(source: T): Readable> { return readable({} as NoStore, 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(source: T, callbacks?: { update: () => void, onDestroy: (() => void)[] }): NoStore { 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; } 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 { if (value) return typeof value.subscribe == "function"; return false; } ```
iacore commented 2 years ago

@LokiMidgard ...FYI https://svelte.dev/repl/dc5b04b90d6e4f7181a55685afd9fab6?version=3.44.3

iacore commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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. :slightly_smiling_face:

gustavopch commented 2 years ago

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 commented 2 years ago

$() 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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

$() 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 commented 1 year ago

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 https://github.com/sveltejs/svelte/issues/4079#issuecomment-851080121

btakita commented 1 year ago

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 https://github.com/sveltejs/svelte/issues/4079#issuecomment-851080121

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 commented 1 year ago

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 commented 1 year ago

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

brunnerh commented 1 year ago

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

Rich-Harris commented 5 months ago

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