Skip to content

Runes: get/set syntax / creating reactive #each items sucksΒ #9240

@brunnerh

Description

@brunnerh

Describe the problem

I just wanted to port a simple to-do sort of demo to runes but it was more difficult than expected and the end result is more verbose than a "Svelte" component should be. Of course I might be doing something wrong.

First naive approach was just one state:

<script>
    let todos = $state([
        { text: 'Item 1', done: false },
        { text: 'Item 2', done: false },
    ]);
    const remaining = $derived(todos.filter(x => x.done == false));
</script>

{#each todos as todo}
    <div>
        <input type="checkbox" bind:checked={todo.done} />
        <input bind:value={todo.text} />
    </div>
{/each}
...
Remaining: {remaining.length}

The bindings in the #each block do nothing.

Next step was trying to use a state for each item:

const todo = text => {
    let item = $state({
        text,
        done: false,
    });
    return item;
}
let todos = $state([
    todo('item 1'),
    todo('item 2'),
]);

Which does not work because the item gets evaluated in the compiled output before it is returned.

So finally, every changing property is stateified:

const todo = initialText => {
    let text = $state(initialText);
    let done = $state(false);
    return {
        get text() { return text }, set text(v) { text = v },
        get done() { return done }, set done(v) { done = v },
    };
}

This finally works but is ...not great, even if we could use arrow functions (as already bemoaned by Rich Harris).

Is this just buggy or will it really be necessary to use $state for every single property?
I imagine this being an issue with JSON returned from a server.

Describe the proposed solution

If it is necessary to declare that many properties, that could probably be done by the compiler, e.g. with an additional rune.

const todo = initialText => {
    let text = $state(initialText);
    let done = $state(false);
    return $access({ text, done });
}

Should then be spreadable, too, e.g.

const todo = (id, initialText) => {
    let text = $state(initialText);
    let done = $state(false);
    return { id, ...$access({ text, done }) };
}

$access could possibly be split into something like $readable/$writable to control whether setters are generated.

Alternatives considered

  • Use one $state and bind to that indexed:
    let todos = $state([
        { text: 'Item 1', done: false },
        { text: 'Item 2', done: false },
    ]);
    {#each todos as _, i}
        <div>
            <input type="checkbox" bind:checked={todos[i].done} />
            <input bind:value={todos[i].text} />
        </div>
    {/each}
  • Have one state per item, but wrap it to preserve reactivity (pretty ugly).

Importance

nice to have

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions