Logo

dev-resources.site

for different kinds of informations.

"Helper" Varaibles in Svelte 5

Published at
11/8/2024
Categories
svelte
sveltekit
tutorial
javascript
Author
kvetoslavnovak
Author
14 person written this
kvetoslavnovak
open
"Helper" Varaibles in Svelte 5

Bye Bye Magical Svelte 4 $:

Following my recent post Experiences and Caveats of Svelte 5 Migration I would like to highlight some techniques and change of mindset when going from Svelte 4 to Svelte 5.

Svelte 4 uses "magical" $: and let and does all the heavy lifting to make code reactive. We also embraced varaibles reassignment like

<script>
let arr = [1, 2, 3]
let value = 4

arr = [...arr, value]
</script>
Enter fullscreen mode Exit fullscreen mode

instead of methods updating/mutating varaibles like push etc.

I was quite suprprised to re-learn good old JS patterns using Svelte 5.

No Need to Be Reactive All the Time

And I was also probably quite spoiled by let in Svelte 4 , no reasoning about reactivity, it was included if needed. But not all varaibles have to be reactive. Also non reactive variables may be updated in reactive or even "traditional mutating code". The real need for reactive variable is when we use it in UI (when this varaible is rendered in html/page and we need it to update later).

You may encounter erros in Svelte 5 like Cannot assign to derived state, State referenced in its own scope will never update. Did you mean to reference it inside a closure? or derived_references_self\nA derived value cannot reference itself recursively if using Svelte 4 coding style.

Example of Helper Variables

Take a look at this example of Svelte 4 style of code:

<script>
    let value;

    let derivedArr = []
    $: if (value) {
        derivedArr = [...derivedArr, value]
    }

    function random () {
        value = Math.floor(1 + Math.random() * 10)
    }
</script>

<button on:click={random}>Generate Random Value</button>
<p>value: {value}</p>
<p>derivedArr: {derivedArr}</p>
Enter fullscreen mode Exit fullscreen mode

DEMO

The random function works as some kind of simulation of server response sending as a result of a search query, on the client we are accumulating the results, so it work like a "Load more" button. Hereby random function should not be changed. We have two reactive variables and Svelte 4 solves the updates automatically. We only needed to remember that the right way is by reassigning the variable.

In Svelte 5 we should think a little how to achieve the same result. The two variables we are using are not enough, we need one more, the helper one.

One way is to use a $derived rune.

<script>
    let value = $state();


                  let helperArr = [];   
let derivedArr =  $derived.by(() => {
                   if (value) {
                     helperArr.push(value);
                     return helperArr;
                   }
                  });

    function random () {
        value = Math.floor(1 + Math.random() * 10)
    }
</script>

<button onclick={random}>Generate Random Value</button>
<p>value: {value}</p>
<p>derivedArr: {derivedArr}</p>
Enter fullscreen mode Exit fullscreen mode

DEMO

I am trying visually emphasize which parts of the code are relevant and somehow "wrapped" from Svelte 4 into Svelte 5 mental model. helperArr is declared outside of a$derived.by() function scope not to be reseted every time $derived.by() reruns (which is when reactive variables inside $derived updates). If you know easier way to do this let me know.

There is also an $effect() rune way to achieve the same with untrack trick. It might look even simplier but we should avoid effects if possible (mainly Svetlet 5 effects do not run on the server/SSR).

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

    let value = $state();
    let derivedArr = $state([]);

    $effect.pre(() => {
        if (value)
            untrack(() => derivedArr.push(value))
    });

    function random () {
        value = Math.floor(1 + Math.random() * 10)
    }
</script>

<button onclick={random}>Generate Random Value</button>
<p>value: {value}</p>
<p>derivedArr: {derivedArr}</p>
Enter fullscreen mode Exit fullscreen mode

DEMO

Real Life Example

This is the example how I tried to migrate quite stright forward Svelte 4 page to Svelte 5. It took me a while to rethink the code. This page works as a posts search with a "Load More" functionality (adding results or pagging if a user does not have JS):

Svelte 4

<script>
    import Icon from '../components/Icon.svelte';
    import { enhance } from '$app/forms';
    import { tick } from 'svelte';

    export let form;
    export let searchingLang;
    export let l;

    let results = [];
    let previousSearch = '';
    let searchTerm;
    let skip;

    $: if (!!form && form?.thereIsMore) {
        searchTerm = form.searchTerm;
        skip = Number(form?.skip) + 20;
    }

    $: if (!!form?.searchResultFromAction) {
        if (previousSearch == form.searchTerm && form.thereWasMore) {
            results = [...results, ...form.searchResultFromAction];
        } else {
            results = [...form.searchResultFromAction];
            previousSearch = form.searchTerm;
        }
    }

    async function intoView(el) {
        await tick();
        if (el.attributes.index.nodeValue == skip - 20 && skip != undefined) {
            el.scrollIntoView({ behavior: 'smooth' });
        }
    }
</script>

{#if results.length}
    <ol>
        {#each results as item, index}
            <li use:intoView {index} aria-posinset={index}>
                <!-- users without javascript have calculated order of results within paggination and css disables standard ol ul numbering -->
                <!-- users with javascript have standard ol ul numbering and loading more feature -->
                <noscript>{Number(index) + 1 + Number(form?.skip)}. </noscript>
                <a href="/post/{searchingLang}/{item.id}/content">{item.title}</a>
            </li>
        {/each}
    </ol>

    {#if form?.thereIsMore}
        <form
            method="POST"
            action="?/search&skip={skip}&thereWasMore={form?.thereIsMore}"
            use:enhance
            autocomplete="off"
        >
            <label>
                <!-- Probably we do not need to bind the value as this is hidden input -->
                <!-- <input name="searchTerm" type="hidden" bind:value={searchTerm} /> -->
                <input name="searchTerm" type="hidden" value={searchTerm} />
            </label>
            <button aria-label="Button to load more search results" class="outline">
                <Icon name="loadMore" />
            </button>
        </form>
    {/if}
{:else if form?.searchResultFromAction.length == 0}
    {l.noResultsFound}
{/if}

<style>
    @media (scripting: none) {
        /* users without javascript have calculated order of results within paggination and css disables standard ol ul numbering
users with javascript have standard ol ul numbering and loading more feature */
        ol {
            list-style-type: none;
        }
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Svelte 5

<script>
    import Icon from '../components/Icon.svelte';
    import { enhance } from '$app/forms';
    import { tick } from 'svelte';

    let { form, searchingLang, l } = $props();

    let previousSearch = '';
    let skip = $derived.by(() => {
        if (!!form && form?.thereIsMore) {
            return Number(form?.skip) + 20;
        }
    });

    let helperResultsArr = [];
    let results = $derived.by(() => {
        if (!!form?.searchResultFromAction) {
            if (previousSearch == form.searchTerm && form.thereWasMore) {
                helperResultsArr.push(...form.searchResultFromAction);
                return helperResultsArr;
            } else {
                helperResultsArr = [];
                helperResultsArr.push(...form.searchResultFromAction);
                previousSearch = form.searchTerm;
                return helperResultsArr;
            }
        } else return [];
    });

    async function intoView(el) {
        await tick();
        if (el.attributes.index.nodeValue == skip - 20 && skip != undefined) {
            el.scrollIntoView({ behavior: 'smooth' });
        }
    }
</script>

{#if results.length}
    <ol>
        {#each results as item, index}
            <li use:intoView {index} aria-posinset={index}>
                <!-- users without javascript have calculated order of results within paggination and css disables standard ol ul numbering -->
                <!-- users with javascript have standard ol ul numbering and loading more feature -->
                <noscript>{Number(index) + 1 + Number(form?.skip)}. </noscript>
                <a href="/post/{searchingLang}/{item.id}/content">{item.title}</a>
            </li>
        {/each}
    </ol>

    {#if form?.thereIsMore}
        <form
            method="POST"
            action="?/search&skip={skip}&thereWasMore={form?.thereIsMore}"
            use:enhance
            autocomplete="off"
        >
            <label>
                <input name="searchTerm" type="hidden" value={form.searchTerm} />
            </label>
            <button aria-label="Button to load more search results" class="outline">
                <Icon name="loadMore" />
            </button>
        </form>
    {/if}
{:else if form?.searchResultFromAction.length == 0}
    {l.noResultsFound}
{/if}

<style>
    @media (scripting: none) {
        /* users without javascript have calculated order of results within paggination and css disables standard ol ul numbering
users with javascript have standard ol ul numbering and loading more feature */
        ol {
            list-style-type: none;
        }
    }
</style>
Enter fullscreen mode Exit fullscreen mode

That is all for now.

PS: Do not hesitate to let me know if you would do the migration in a different way.

sveltekit Article's
30 articles in total
Favicon
Optimize SvelteKit performance with brotli compression
Favicon
SvelteKit VS Astro. laidback side by side
Favicon
Integrating SvelteKit with Storyblok (Using Svelte 5)
Favicon
Make EditorJS work in Svelte(kit) SSR
Favicon
Why Svelte?
Favicon
Nosecone: a library for setting security headers in Next.js, SvelteKit, Node.js, Bun, and Deno
Favicon
Building AI-Powered Apps with SvelteKit: Managing HTTP Streams from Ollama Server
Favicon
NgSysV2-3.4: A Serious Svelte InfoSys: Rules-friendly version
Favicon
NgSysV2-3.3: A Serious Svelte InfoSys: Firebase D/b rules and Login
Favicon
NgSysV2-3.6: A Serious Svelte InfoSys: Deploying to the Google Cloud
Favicon
NgSysV2-3.5: A Serious Svelte InfoSys: Client-Server Version
Favicon
NgSysV2-4.2: SEO (Search Engine Optimisation)
Favicon
NgSysV2-4.3: Automated Svelte Pre-render Builds
Favicon
NgSysV2-4.4: Responsive/Adaptive Design
Favicon
Deploy a Static Sveltekit site to Railway
Favicon
Why You Should Avoid Using `try...catch` in SvelteKit Actions
Favicon
How to integrate shadcn-svelte into the editable.website template
Favicon
PostgreSQL Full Text Search Rank by Position
Favicon
How to Build a Content-Driven Static Site with Markdown, SvelteKit and Fusionable
Favicon
Interview with Prabhu Kiran Konda, Creator of Snail AI!
Favicon
"Helper" Varaibles in Svelte 5
Favicon
Experiences and Caveats of Svelte 5 Migration
Favicon
Running a Function When an #await Block resolves in Svelte(Kit)
Favicon
SanS-UI v0.0.1 Quick Start!
Favicon
Introduction to Svelte: Features and Benefits of the Modern JavaScript Framework
Favicon
Sveltekit + TypeScript + TypeORM + ESM
Favicon
Svelte 5 is out!!!
Favicon
SanS-UI Released v0.0.1
Favicon
How to Integrate Passkeys into SvelteKit
Favicon
AWS Amplify (Gen2) with SvelteKit: authentication on SSR

Featured ones: