DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on • Edited on

4 1 1 1

Sharing Runes in Svelte 5 with the Rune Class

I generally don't use classes in any TypeScript I write. I believe functions make things simpler, and you no longer get Tree Shaking with methods in a class.

However, using classes with Runes can actually be faster, because they don't have to compile $state variables with get and set or with value... they just work. This is what Rich Harris recommends in many cases.

Sharable Rune

We need a sharable Rune class, and of course we must use Context.

// rune.svelte.ts
import { getContext, hasContext, setContext } from "svelte";


type RCurrent<TValue> = { current: TValue };

export class Rune<TRune> {

    readonly #key: symbol;

    constructor(name: string) {
        this.#key = Symbol(name);
    }

    exists(): boolean {
        return hasContext(this.#key);
    }

    get(): RCurrent<TRune> {
        return getContext(this.#key);
    }

    init(value: TRune): RCurrent<TRune> {
        const _value = $state({ current: value });
        return setContext(this.#key, _value);
    }

    // NOT NEEDED!
    update(getter: () => TRune): void {
        const context = this.get();
        $effect(() => {
            context.current = getter();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

And we can export custom runes where we want.

// counter.svelte.ts
import { Rune } from "./rune.svelte";

export const counter = new Rune<number>('counter');
Enter fullscreen mode Exit fullscreen mode

This is how most people share $state variables anyway, however, this is safe for the server (see my previous posts in this thread). We must name it like any other context.

Initialize

We must initialize our $state just like anywhere else, only once, in the parent component.

<script lang="ts">
    import { counter } from '$lib/counter.svelte';

    const count = counter.init(0);

    // `count.current` is available here
</script>
Enter fullscreen mode Exit fullscreen mode

Read Anywhere

We can use it safely in a child component and read the current method.

<script lang="ts">
    import { counter } from '$lib/counter.svelte';

    const count = counter.get();
</script>

<h1>Hello from Child: {count.current}</h1>

<button type="button" onclick={() => count.current++}>
  Increment From Child
</button>
Enter fullscreen mode Exit fullscreen mode

Update Anywhere

You can update the rune using the current method as normal.

<script lang="ts">
    import { counter } from '$lib/counter.svelte';

    const count = counter.get();

    count.current = 9;
</script>
Enter fullscreen mode Exit fullscreen mode

Derived Update

Sometimes, you may need to update a value based on another reactive value. 99% of use cases, you should just use the .current value, but here is an example just in case this applies to you.

<script lang="ts">
    import { counter } from '$lib/counter.svelte';

    let value = $state(8);

    counter.update(() => value);
</script>

<h1>Hello from Child2: {value}</h1>

<button type="button" onclick={() => value++}> 
    Update From Another State
</button>
Enter fullscreen mode Exit fullscreen mode

I hope you find value,

J

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs