DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on

Using Sharable Runes with TypeScript in Svelte5

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.

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!

Top comments (9)

Collapse
 
brugh profile image
brugh

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?

Collapse
 
jdgamble555 profile image
Jonathan Gamble

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

Collapse
 
brugh profile image
brugh

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...

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

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.

Thread Thread
 
brugh profile image
brugh • Edited

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 ...

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

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....

Thread Thread
 
brugh profile image
brugh

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.

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble

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.

Collapse
 
jamesforan profile image
James Foran

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".