DEV Community

Cover image for Svelte journey | Bindings, Lifecycles, Stores
Denys Sych
Denys Sych

Posted on • Edited on

Svelte journey | Bindings, Lifecycles, Stores

Basics: Bindings, Lifecycles, Stores

Welcome here,

This article marks the finishing point in covering the most basic Svelte stuff. Recently, we found out about props, template directives, events, and today we are going to review bindings, lifecycles, and stores.

Bindings

What is this

Two-way binding like <my-input [(value)]=”value”></my-input> in Angular or <Input value={name} onChange={setName} /> in React:

<script>
    let name = 'world';
</script>
<input bind:value={name} />
<h1>Hello {name}!</h1>
Enter fullscreen mode Exit fullscreen mode

Automatically converts value type

In DOM everything is a string, but Svelte converts value’s type according to type attribute:

<input type="number" bind:value={a} min="0" max="10" /> // number
<input type="range" bind:value={a} min="0" max="10" /> //number
<input type="checkbox" bind:checked={yes} /> // boolean
<select bind:value={selected}   on:change={() => answer = ''} /> // object
<textarea bind:value></textarea> // string, shorthand form
<textarea bind:value={value}></textarea> // string, full form
Enter fullscreen mode Exit fullscreen mode

Select initial value

Default value for selected in <select bind:value={selected}... /> is first entry. Still, while bindings are initialised, selected remains undefined, be careful, use Elvis ?: selected?.id.

Enabling <select /> multi select ability

<select multiple bind:value={flavours}>
  {#each ['vanilla', 'coffee'] as flavour}
        <option>{flavour}</option>
    {/each}
</select>
Enter fullscreen mode Exit fullscreen mode

Group inputs (radio groups, checkbox groups)

{#each ['vanilla', 'coffee'] as flavour}
    <label>
        <input
            type="checkbox"
            name="flavours"
            value={flavour}
            bind:group={flavours} // it is an array
        />

        {flavour}
    </label>
{/each}
Enter fullscreen mode Exit fullscreen mode

Lifecycle

  • onMount does not run inside a server-side component;
  • onMount must be called during the component's initialisation, but doesn't need to live inside the component.
<script>
    import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte';
    import { paint } from './gradient.js';

    ...

    // After the component is first rendered to the DOM
    onMount(() => {
        doThings();
        return () => cleanup();
    });
    // Immediately before the DOM is updated. Before mount, too.
    beforeUpdate(() => {
        if (thing) {
            process(thing);
        }
    });
        // Once the DOM is in sync with your data
    afterUpdate(() => {
        if (thing) {
            process(thing);
        }
    });
    // tick() usage example: Await in-progress DOM changes
    async function handleSelection() {
        someStateUpdates();
        await tick();
        performSomeOperationThatRequiresUpdatedDOM();
    }
    // On destroy
    onDestroy(anotherCleanup);
});
</script>
Enter fullscreen mode Exit fullscreen mode

Also, lifecycle functions can be called not only in the root ... but also from some kind of callback. Also, they can be called multiple times. For example, the next code sample is absolutely valid:

<script>
    let items = new Set();

    onMount(() => {
        // ...
    });

    function addItem(fn) {
        onMount(() => {
            items.add(fn);
            return () => items.delete(fn);
        });

        afterUpdate(async () => {
            // ...
            await tick();
            // ...
        });
    }
    //...
</script>
Enter fullscreen mode Exit fullscreen mode

Component state update ≠ update the DOM immediately

Instead, it waits until the next microtask to see if there are any other changes that need to be applied, including in other components (batching and optimization).

To await updated DOM in the same function execution, we can use await tick(), which resolves when pending DOM updates are completed.

Stores

Dealing with data management, avoid prop drilling.

// stores.js ------------------------------------------------------------
import { writable } from 'svelte/store';
export const count = writable(0);
// Demo.svelte ------------------------------------------------------------
<script>
    import { count } from './stores.js';

        let count_value;

    const unsubscribe = count.subscribe((value) => { // manual subscription (better to use auto-sub $count)
        count_value = value;
    });
    onDestroy(unsubscribe); // requires unsubscription on destroy

    const reset = () => count.set(0);
  const increment = () => count.update((n) => n + 1);

    $: console.log("count is " + $count); // Auto-sub in script
</script>
...
<h1>The count is {count_value}</h1>
<h1>The count is {$count}</h1> // Auto-sub in markup
Enter fullscreen mode Exit fullscreen mode

$value Auto-subscription

  • Works only with store variables that are declared (or imported) at the top-level scope of a component;
  • $ is reserved prefix and it always assumed to refer to a store value.

Writable, Readable, Derived stores

All stores are consumed in the same manner (with different capabilities).

  • Writable as in the example — you can both read and write inside consumer code. const count = writable(initialValue?);
  • Readable — readonly in consumers. const value = readable(initialValue?, startFn, endFn) where:
    • initialValue - initial value, can be null or undefined;
    • startFn - is called when first subscribe happens (Like initialisation of Hot Observable in RxJS);
    • endFn - is called on last unsubscribe.
  • Derived — on one hand, like selectors in Redux. On the other hand, can set value to itself instead of returning it (useful for async derive). const doubled = derived(stores,deriveFn, initialValue?)

Derived store specifics

Set value

Derived stores can set value? This part was not fully clear at first, but after some exploration I’ve figured out that it is about that you can just derive in async way: instead of instant convert you may hit an API or perform any other async. operation. In that case you can’t return promise or callback to tell that it is async. But what you can do is a call set or update in your’s async operation callback to emit the desired output.

import { derived } from 'svelte/store';

const delayed = derived(
    a,
    ($a, set) => {
        setTimeout(() => set($a), 1000);
    },
    2000
);

const delayedIncrement = derived(a, ($a, set, update) => {
    set($a);
    setTimeout(() => update((x) => x + 1), 1000);
    // every time $a produces a value, this produces two
    // values, $a immediately and then $a + 1 a second later
});
Enter fullscreen mode Exit fullscreen mode

Can return function from deriveFn

It will be called when a) the callback runs again, or b) the last subscriber unsubscribes.

import { derived } from 'svelte/store';
const tick = derived(
    frequency,
    ($frequency, set) => {
        const interval = setInterval(() => {
            set(Date.now());
        }, 1000 / $frequency);
        return () => {
            clearInterval(interval);
        };
    },
    2000
);
Enter fullscreen mode Exit fullscreen mode

Custom stores

Custom store is a conventional name in Svelte for such kind of facade that is the code snippet below. Nothing special.

function createCount() {
    const { subscribe, set, update } = writable(0);

    return {
        subscribe,
        increment: () => update((n) => n + 1),
        decrement: () => update((n) => n - 1),
        reset: () => set(0)
    };
}
Enter fullscreen mode Exit fullscreen mode

Store bindings

Actual for writable stores. Usage is the same as with local variables, just prefix with $:

export const name = writable('world');
...
<input bind:value={$name}>

<button on:click={() => $name += '!'}> // same as: name.set($name + '!')
    Add exclamation mark!
</button>
Enter fullscreen mode Exit fullscreen mode

That's all for today, take care, go Svelte!

Resources

Svelte Tutorial
Svelte Documentation | Stores

Top comments (0)