Lilleman
Lilleman

Reputation: 8121

How does Svelte reactivity work inside functions?

I have some serious problems with reactivity in Svelte. I have isolated what I think is at least one of my main issues. When bind:ing a variable to a checkbox, the reactivity seems to break when setting the variable inside a function, but not outside. Is this intended behavior? In that case why? And what is the intended work flow?

Example code, a Svelte component:

<script>
    let foo = true;

    // This assignment works both on the plain text view (Foo: true/false)
    // and on the checkbox
    // setInterval(() => foo = !foo, 500)

    // This assignment works only on the plain text view (Foo: true/false)
    // but not for the checkbox
    function action() {
        foo = !foo;
    }
</script>

Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } on:click|preventDefault={ action } />

Svelte REPL to see this problem in action: https://svelte.dev/repl/73de7d705ab3442786710cd88ea3f625?version=3.12.1

Upvotes: 12

Views: 2824

Answers (5)

Eric Mockler
Eric Mockler

Reputation: 36

This binding issue isn't with Svelte's reactivity, it's using preventDefault on the click event. The HTML checked attribute never changes, because the checked attribute doesn't fire any JS events to update the DOM.

In Svelte, you don't need to set foo since it's already bound to the HTML state.

...
function action() {
        console.log(foo)
    }
</script>

Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } on:change={ action } />

But if you don't want HTML to change before something else, declaring action as async works fine with on|click|preventDefault:

async function action() {
        let data = await new Promise((resolve) => setTimeout(resolve, 500))
        foo = !foo
        console.log("changed!")
        return data
    }

Upvotes: 1

C&#225;ssio Tavares
C&#225;ssio Tavares

Reputation: 179

The best solution is much simpler, because the two-way binding by itself does everything you need. So, this does it:

<script>
  let foo = true;
</script>

Foo: { foo }<br />
Checkbox: <input type="checkbox" bind:checked={ foo } />

I know the question is two years old, but I think it needed better closure.

Upvotes: 3

Max
Max

Reputation: 1523

I would suggest not to cancel the event with the usage of preventDefault, because it would stop the default behaviour of the checkbox.

And therefore the reactivity is broken. If you wrap the reassignment of foo in a setTimeout you just tricking the event loop and the default is not prevented.

So if you like to stop the propagation of the event use preventDefault with the suggestion from Filip.

    <script>
        let foo = true;

        // This assignment works both on the plain text view (Foo: true/false)
        // and on the checkbox
        // setInterval(() => foo = !foo, 500)

        // This assignment works only on the plain text view (Foo: true/false)
        // but not for the checkbox
        async function action(e) {
                e.preventDefault();
                setTimeout(() => {foo =! foo});

        }
    </script>

    Foo: { foo }<br />
    Checkbox: <input type="checkbox" bind:checked={ foo } on:click={ action } />

Upvotes: 0

Stephane Vanraes
Stephane Vanraes

Reputation: 16411

Seems your problem is cause by the bind: option. Having this will cause the foo to be updated twice: once for the 'action' and once behind the scenes when the binding kicks in.

Just removing the bind: should work

<script>
    let foo = true;

    // This assignment works both on the plain text view (Foo: true/false)
    // and on the checkbox
    // setInterval(() => foo = !foo, 500)

    // This assignment works only on the plain text view (Foo: true/false)
    // but not for the checkbox
    function action() {
        setTimeout(() => foo = !foo, 1000);
    }
</script>

Foo: { foo }<br />
Checkbox: <input type="checkbox" checked={ foo } on:click|preventDefault={ action } />

This make sense because you do not want to have two-way-binding for the checked state to foo. You only want the checkbox to reflect the value of foo.

Upvotes: 1

Filip
Filip

Reputation: 2522

If you wrap your variable declaration in a setTimeout without any time limit (making it default to 0 and therefore run as soon as possible) your code will work.

function action() {
    setTimeout(() => {
        foo = !foo;
    });
}

I wish I could explain why this works, but I can't...

Upvotes: 3

Related Questions