Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Svelte reactivity not triggering when variable changed in a function

I am a bit confused here and unluckily I couldn't get any solution on the discord channel of svelte so here I go...

I have a rather basic example of two classes, let them be App and Comp. App creates a Comp instance and then updates this instance's value after a button click.

The Comp instance should set this value to a different variable (inputValue) and upon changing that variable it should fire validate(inputValue) which is reactive. Here is a REPL: https://svelte.dev/repl/1df2eb0e67b240e9b1449e52fb26eb14?version=3.25.1

App.svelte:

<script>
    import Comp from './Comp.svelte';
    
    let value = 'now: ' + Date.now();
    
    function clickHandler(e) {
        value = 'now ' + Date.now();
    }
</script>

<Comp
    bind:value={value}
/>
<button type="button" on:click={clickHandler}>change value</button>

Comp.svelte:

<script>
    import { onMount } from 'svelte';

    export let value;

    let rendered = false;
    let inputValue = '';

    $: validate(inputValue); // This doesn't execute. Why?

    function validate(val) {
        console.log('validation:', val); 
    }

    onMount(() => {
        rendered = true;
    });

    $: if (rendered) {
        updateInputValue(value);
    }

    function updateInputValue(val) {
        console.log('updateInputValue called!');
        if (!value) {
            inputValue = '';
        }
        else {
            inputValue = value;
        }
    }
</script>

<input type="text" bind:value={inputValue}>

So as soon as the value is changed:

  1. The reactive if (rendered) {...} condition is called
  2. updateInputValue is called and inputValue is changed. HTML input element is updated to this value.
  3. validate(inputValue) never reacts to this change - WHY?

If I omit the extra call to the updateInputValue function in the reactive if (rendered) condition and put updateInputValue function body's code directly to the condition, then validate(inputValue) is triggered correctly, i.e.:

// works like this  
$: if (rendered) {
    if (!value) {
        inputValue = '';
    }
    else {
        inputValue = value;
    }
}

So how come it doesn't work when updated in the function?

like image 829
Fygo Avatar asked Dec 14 '22 08:12

Fygo


2 Answers

@johannchopin's answer has revealed the issue.


You can read my blogpost for slightly in-depth explanation of how reactive declaration works, here's the tl;dr:

  • reactive declarations are executed in batch.

    svelte batches all the changes to update them in the next update cycle, and before it updates the DOM, it will execute the reactive declarations to update the reactive variables.

  • reactive declarations are executed in order of their dependency.

    reactive declarations are executed in batch, and each declaration is executed once. some declarations are depending on each other, eg:

    let count = 0;
    $: double = count * 2;
    $: quadruple = double * 2;
    

    in this case, quadruple depends on double. so regardless of order of your reactive declarations, $: double = count * 2; before $: quadruple = double * 2 or the other way round, the former should be and will be executed before the latter.

    Svelte will sort the declarations in the dependency order.

    In cases where there's no dependency relationship with each other:

    $: validate(inputValue);
    $: if (rendered) updateInputValue(value);
    

    1st statement depends on validate and inputValue, 2nd statement depends on rendered, updateInputValue and value, the declaration is left in the order as it is.


Now, knowing this 2 behaviors of reactive declaration, let's take a look at your REPL.

As you change inputValue, rendered or value, Svelte will batch the changes, and start a new update cycle.

Right before updating the DOM, Svelte will execute all the reactive declaration in 1 go.

Because there's no dependency between the validate(inputValue); and if (rendered) updateInputValue(value); statements, (as explained earlier), they will be executed in order.

If you change rendered or value only, the 1st statement (validate(inputValue)) will not be executed, and similarly, if you change inputValue the 2nd statement (if (rendered) updateInputValue(value)) will not be executed.

Now, in the updateInputValue you change the value of inputValue, but because we are already in an update cycle, we wont start a new one.

This usually isn't a problem, because if we sort reactive declarations in dependency order, the statement that updates the variable depended on will be executed before the statement that relies on the variable.


So, knowing what's wrong, there's a few "solutions" you can go for.

  1. Manually reorder the reactive declaration statements, especially when there's an implicit dependency of the order of execution.

See the difference of ordering the reactive declaration in this REPL

So change your REPL to:

$: if (rendered) {
  updateInputValue(value);
}
$: validate(inputValue);

See REPL

  1. Explicitly defining the dependency in the reactive declarations
$: validate(inputValue);

$: if (rendered) {
    inputValue = updateInputValue(value);
}
    
function updateInputValue(val) {
    console.log('updateInputValue called!');
    if (!value) {
        return '';
    }
    else {
        return value;
    }
}

See REPL

like image 52
Tan Li Hau Avatar answered Mar 16 '23 00:03

Tan Li Hau


Really strange (and I couldn't really explain it) but if you put the reactive statement $: validate(inputValue); after the function updateInputValue declaration, it's working as expected:

<script>
    import { onMount } from 'svelte';
    
    export let value;
    
    let rendered = false;
    let inputValue = '';

    function validate(val) {
        console.log('validation:', val);
    }
    
    onMount(() => {
      rendered = true;
   });
    
    $: if (rendered) {
        updateInputValue(value);
    }
    
    function updateInputValue(val) {
        console.log('updateInputValue called!');
        if (!value) {
            inputValue = '';
        }
        else {
            inputValue = value;
        }
    }
    
    $: validate(inputValue);
</script>

Check this REPL.

like image 31
johannchopin Avatar answered Mar 15 '23 23:03

johannchopin