Search code examples
stateuse-effectsvelte

How to listen the state changes in svelte like useEffect


I have read some article about state change listener, As I am a very beginner to the svelte environment I can't figure out what is the most efficient way to listen to the state change.

Let us take state variable as X and Y

Method 1:

$: if (X||Y) {
    console.log("yes");
}

Method 2:

Use a combination of afterUpdate and onDestroy

REPL: https://svelte.dev/repl/300c16ee38af49e98261eef02a9b04a8?version=3.38.2

import { afterUpdate, onDestroy } from 'svelte';

export function useEffect(cb, deps) {
    let cleanup;
    
    function apply() {
        if (cleanup) cleanup();
        cleanup = cb();
    }
    
    if (deps) {
        let values = [];
        afterUpdate(() => {
            const new_values = deps();
            if (new_values.some((value, i) => value !== values[i])) {
                apply();
                values = new_values;
            }
        });
    } else {
        // no deps = always run
        afterUpdate(apply);
    }
    
    onDestroy(() => {
        if (cleanup) cleanup();
    });
}

Method 3:

Use writable and subscribe


<script>
import { writable } from 'svelte/store';
const X = writable(0);
const Y = writable(0);


X.subscribe(value => {
    console.log("X was changed", value);
});


Y.subscribe(value => {
      console.log("Y was changed", value);
});

</script>

<button on:click={(e)=>{
    X.update((val)=>val++)
}}>Change X</button>
<button on:click={(e)=>{
    Y.update((val)=>val++)
}}>Change Y</button>

Solution

  • Reactive statements

    Svelte does not have an equivalent of useEffect. Instead, Svelte has reactive statements.

    // Svelte
    // doubled will always be twice of single. If single updates, doubled will run again.
    $: doubled = single * 2
    
    // equivalent to this React
    
    let single = 0
    const [doubled, setDoubled] = useState(single * 2)
    
    useEffect(() => {
      setDoubled(single * 2)
    }, [single])
    

    This may seem like magic, since you don't define dependencies or teardown functions, but Svelte takes care of all that under the hood. Svelte is smart enough to figure out the dependencies and only run each reactive statement as needed.

    So if you want to run a callback every time a value updates, you can simply do this.

    <script>
      let value = ''
      $: console.log(value)
    </script>
    
    <input type='text' name='name' bind:value />
    

    In short, this will console log the value of the input every time the input's value changes.

    Recreating your suggestions

    Method 1

    That's about as concise as you can go, and is my go-to means of listening to state changes unless I need something more complex.

    $: if(x || y) console.log('yes')
    

    Though note that there may be a subtle bug here. If x or y were both truthy then turn falsy (e.g., they both became empty strings), this statement will not run.

    Method 2

    Here, you basically recreated React's useEffect. This works, but you can simplify the implementation in your REPL a lot using reactive statements.

    <script>
        let count = 1;
        $: console.log(count)
    </script>
    
    <input type="number" bind:value={count}>
    

    Method 3

    Stores are great for passing data between components without using props or context. Check out this answer for more: https://stackoverflow.com/a/67681054/11506995

    In this case, though, we can simplify everything using regular reactive context (again).

    <script>
      let x = 0
      let y = 0
    
      $: console.log('x was changed', x)
      $: console.log('y was changed', y)
    </script>
    
    <button on:click={() => x++}>Change x</button>
    <button on:click={() => y++}>Change x</button>
    

    I also have a detailed answer of comparing React's context/useEffect with Svelte's context/stores/reactive statements in Understanding Context in Svelte (convert from React Context)

    https://stackoverflow.com/a/67681054/11506995