DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on • Updated on

Create the Perfect Sharable Rune in Svelte

Singleton Server Issue

When Svelte 5 comes out soon, it seems Runes (Signals) will share one of the old problems Stores had: it could be dangerous to share the signal on the server. I want get into the details here, but you can read The Correct Way to Use Stores in SvelteKit.

Basically, you need to use context when handling shared stores in an outside script file. Otherwise your data could be seen by other users on the server, instead of just shared across one user.

Get and Set

However, as Fireship pointed out, there is another pain issue with runes. You have to use get and set, along with a value method, which is different from just using $state.

Let's say I want to do this:

app.svelte

<script>
let count = $state(0)
count++
</script>

<button type="button" on:click={() => count++}>
  Increment
</button>

<p>Count {count}</p>
Enter fullscreen mode Exit fullscreen mode

Demo


No problem, as the count will work as expected. You can't set a value on a variable, as JavaScript will just end up reassigning it. Svelte actually compiles to use methods under the hood (see JS Output tab). Therefore, trying to change how this works after compilation will require an actual method: value. This is exactly how SolidJS Signals, Angular Signals, Qwik Signals, Preact Signals, and Vue Ref work normally; there is a value method.

Rich Harris had a response to Fireship, although I'm not really sure he solved the problem. You still need to create the get and set, and return an object:

app.svelte

<script>
    import { count } from './rune.js'
    import Child from './Child.svelte'

    count.value++

</script>

<button type="button" on:click={() => count.value++}>
    Increment
</button>
<h1>Hello from Parent: {count.value}</h1>
<Child />
Enter fullscreen mode Exit fullscreen mode

rune.js

let _rune = $state(0)

export const count = {
    get value() {
        return _rune
    },
    set value(newVal) {
        _rune = newVal
    }
}
Enter fullscreen mode Exit fullscreen mode

child.svelte

<script>
    import { count } from './rune.js'

    count.value++

</script>

<h1>Hello from Child: {count.value}</h1>
Enter fullscreen mode Exit fullscreen mode

Demo


But, as you probably don't immediately see, this will cause problems with sharing state on the server.

So... I propose a solution to both. Create a reusable component that can be shared safely on the server.

app.svelte

<script>
    import { rune } from './rune.js'
    import Child from './Child.svelte'

    let count = rune(0)

    count.value++

</script>

<button type="button" on:click={() => count.value++}>
    Increment
</button>
<h1>Hello from Parent: {count.value}</h1>
<Child />

Enter fullscreen mode Exit fullscreen mode

rune.js (reusable wrapper)

import { getContext, hasContext, setContext } from "svelte"

export const rune = (
    startValue, 
    context = 'default'
) => {
    if (hasContext(context)) {
        return getContext(context)
    }
    let _state = $state(startValue)
    const _rune = {
        get value() {
            return _state
        },
        set value(v) {
            _state = v
        }
    }
    setContext(context, _rune)
    return _rune
}
Enter fullscreen mode Exit fullscreen mode

child.svelte

<script>
    import { rune } from './rune.js'

    let count = rune()

    count.value++

</script>

<h1>Hello from Child: {count.value}</h1>
Enter fullscreen mode Exit fullscreen mode

Demo


Now, if Rich Harris put this into the Svelte core code so that you didn't have to use value (just like $state), and it worked as expected compiling to js... that would be great... maybe a $rune().

¯\(ツ)

Notice the second (optional) argument is needed for different runes you want to share (the name of your context). This would allow you to have more than one, and use it as you see fit. This would solve the problem SvelteKit never solved with stores.

TypeScript and Final Thoughts

Svelte Team, if you're reading this, PLEASE PLEASE add a TypeScript version of REPL. This was paintful to write. Even Rich Harris finally mentioned the need for this on the Runes intro video. There are 1,222 questions alone on StackOverflow regarding Typescript with Svelte. This would save developers time, easily sharing correctly typed code. Until then, we can use SvelteLab, although not with Runes... yet.

Runes will be a gamechanger, as everyone agrees... just not quite on the implementation.

J

Currently finishing rebuild of code.build

Top comments (8)

Collapse
 
langpavel_49 profile image
Pavel Lang

TypeScript in REPL will be awesome!
Even in transpile only mode

Collapse
 
evertt profile image
Evert van Brussel

I wonder if things have changed since your last post, but when I try to use this method, I can only use rune() directly inside a component. I can't, for example, make a $lib/counter.svelte.ts with this code:

import { rune } from "./rune.svelte"

export let count = rune(0)

export const increment = () => (count.value += 1)
export const decrement = () => (count.value -= 1)
export const reset = () => (count.value = 0)
Enter fullscreen mode Exit fullscreen mode

And then import those into a component. As soon as I do that I get the error that hasContext is being called outside of component initialization. Which kinda defeats the purpose of this whole abstraction for me. Did it work with an earlier alpha version of Svelte 5?

Collapse
 
jdgamble555 profile image
Jonathan Gamble

See the next post in this series for TS and updates. For a full example - code.build/p/svelte-5-todo-app-wit...

Collapse
 
geweldon profile image
Grant Weldon

Overriding the valueOf() function in your rune code will allow you to access the value from within the svelte brackets without needing to type out the property name.

Image description

Image description

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Yes, but you can't set the new value to something like you can $state, otherwise it would reassign the variable.

Collapse
 
geweldon profile image
Grant Weldon

True, a rather unfortunate downside.

Collapse
 
adminy profile image
Adminy

Why not make pure javascript reactive? No special syntax, no weird data manipulation, after all, if runes means that every JS file gets transpiled again to yet another intermediate js, then making everything in JS reactive just makes sense. Why bother keeping it partially problematic where you're in two states of being and everything is sort of reactive. This way, you observe everything, if it changes, it updates UI.

Collapse
 
pick_avana_fd7118ac87ad6b profile image
Pick Avana

Please update now that release candidate is here.