Using Sharable Runes with TypeScript in Svelte5
Jonathan Gamble

Jonathan Gamble @jdgamble555

About: My main job is not in IT, but I do have a CS degree and have been programming for over 20 years (only born in 1984). I hate dealing with Servers, and want Graph Databases to be the new norm.

Location:
Louisiana
Joined:
Feb 9, 2021

Using Sharable Runes with TypeScript in Svelte5

Publish Date: Apr 28 '24
17 11

I don't know about you, but I don't like using Runes with $state in Svelte 5. Sure, they're easier to set data in your component, but I don't write code and manually put getters and setters.

Single Responsibility Principle

I don't write state inside my component except in small apps. I like to create reusable shared hooks. If it can't be shared, it is still better to follow the SRP for clean coding techniques.

rune.svelte.ts

First, I created a rune that works like Nuxt or Qwik Signals. I don't want to call the variable as a function, and I don't want to call set. The value attribute is the best implementation. You can create your own if you disagree.

export const rune = <T>(initialValue: T) => {

    let _rune = $state(initialValue);

    return {
        get value() {
            return _rune;
        },
        set value(v: T) {
            _rune = v;
        }
    };
};
Enter fullscreen mode Exit fullscreen mode

This is what I use instead of $state everywhere in my app, with the exception of small changes in a component.

Update 11/15/24

You can also use this version for simplification. You MUST create a variable for this to work and return that variable for $state to compile correctly.

export const rune = <T>(initialValue: T) => {
    const _rune = $state({ value: initialValue });
    return _rune;
};
Enter fullscreen mode Exit fullscreen mode

Shared Store

If you follow my posts, than you've seen a version of my shared store. It can be done with Runes as well.

import { getContext, hasContext, setContext } from "svelte";
import { readable, writable } from "svelte/store";
import { rune } from "./rune.svelte";

export const useSharedStore = <T, A>(
    name: string,
    fn: (value?: A) => T,
    defaultValue?: A,
) => {
    if (hasContext(name)) {
        return getContext<T>(name);
    }
    const _value = fn(defaultValue);
    setContext(name, _value);
    return _value;
};

// writable store context
export const useWritable = <T>(name: string, value?: T) =>
    useSharedStore(name, writable, value);

// readable store context
export const useReadable = <T>(name: string, value: T) =>
    useSharedStore(name, readable, value);

// shared rune
export const useRune = <T>(name: string, value: T) =>
    useSharedStore(name, rune, value);
Enter fullscreen mode Exit fullscreen mode

Using this method, you can call useRune in you app for shared state anywhere.

Component 1

const user = useRune('user', { ...user state });
Enter fullscreen mode Exit fullscreen mode

Component 2

const user = useRune('user');
Enter fullscreen mode Exit fullscreen mode

And it will just work!

Custom Runes

You can do the same thing with custom Runes. Let's say I want to keep track of the Firebase user's state, and I want to share it across my app. I don't want to keep calling onIdTokenChanged. I can simply created a shared hook.

const _useUser = (defaultUser: UserType | null = null) => {

    const user = rune(defaultUser);

    const unsubscribe = onIdTokenChanged(
        auth,
        (_user: User | null) => {
            if (!_user) {
                user.value = null;
                return;
            }
            const { displayName, photoURL, uid, email } = _user;
            user.value = { displayName, photoURL, uid, email };
        });

    onDestroy(unsubscribe);

    return user;
};

export const useUser = (defaultUser: UserType | null = null) =>
    useSharedStore('user', _useUser, defaultUser);
Enter fullscreen mode Exit fullscreen mode

Now I can use:

const user = useUser();
Enter fullscreen mode Exit fullscreen mode

Anywhere in my app (hooks or components!), and is SAFE for the server. I believe this should be built into Svelte (and all Frameworks). The closest thing I have seen is useState() in Nuxt --- not to be confused with React.

Hope this helps those that are migrating to Svelte 5. I will be updating my SvelteKit Firebase Todo App article in the coming weeks.

J

See Also:

  • Code.Build - Finishing Firebase Course
  • Newsletter - Subscribe for more Svelte 5 and Firebase tips!

Comments 11 total

  • James Foran
    James ForanJun 10, 2024

    This is the post I have been looking for. Thanks. I am a very part-time programmer and am struggling a little with runes, and sharing state across my app.

    I agree with your point about, "should be standard in libraries/frameworks".

  • brugh
    brughSep 19, 2024

    I have an application that has a service.ts with an eventhandler that impacts the store. I cannot use useRune() or any runelike actions in a service file since you can only use runes in components, how can i get the store defined like this to update from an eventhandler?

    • Jonathan Gamble
      Jonathan GambleSep 19, 2024

      This is a Svelte 5 limitation. You would need to rename service.ts to service.svelte.ts, generally speaking.

      • brugh
        brughSep 19, 2024

        yes, that's true. if you add a
        const docState = $state( {value: null} )
        it will let you do that.
        but if you set
        const docState = useRune('docstate')
        it will error out with a Svelte error: lifecycle_outside_component

        i dont see a way to circumvent that...

        • Jonathan Gamble
          Jonathan GambleSep 19, 2024

          Are you using shared.svelte.ts for the useRune()? Also, I'm not sure where you're putting that, so an example repo would help.

          • brugh
            brughSep 23, 2024

            I made 2 thinking errors; first, if you're using a rune in a service.svelte.ts, you don't need a context for children objects to access it. you can simply export const store=$state({}).
            Secondly, I got confused while testing because I used a const store=$state<Map<string,string>>(new Map()); which didn't work; not because of contexts as I thought, but because a Map change doesn't seem to trigger a change in svelte. When i used a normal object of type { [k: string]: string } it worked like a charm.. old javascript habits ...

            • Jonathan Gamble
              Jonathan GambleSep 24, 2024

              You still want to use context. While it can work, there is a chance it could leak in SSR. See my first post about this and the issue - github.com/sveltejs/kit/discussion....

              • brugh
                brughSep 25, 2024

                I agree, I want to :)
                But how could I use the useSharedStore in an event handler that's not in a component? If I use const user = useUser(); in my service.svelte.ts it will complain about the lifecycle_outside_component. Would this require a component without any html that you would put in layout.svelte that just holds the event handler? seems weird.

                • Jonathan Gamble
                  Jonathan GambleSep 25, 2024

                  If you're getting that error, than you're running useUser() outside a component. You cannot run it in a load function, or inside a function that is not called directly in a component. See my Svelte 5 Repo for an example.

  • slidenerd
    slidenerdJan 24, 2025

    but how will you access this inside +layout.ts when running in the browser. Both getContext and setContext throw an error. Use case infinite scrolling. Scroll 3 pages, click on the 31st item. It calls +layout.ts that attempts to navigate to this item which doesnt exist, somehow these items have to be shared between +layout.ts and +layout.svelte

    • Jonathan Gamble
      Jonathan GambleJan 24, 2025

      Read my other posts, you shouldn't use context inside +page.ts or +layout.ts. Context is meant for sharing in components. You can share data in a loader differently by returning the data.

Add comment