Svelte 5 Patterns (Part 1): Simple Shared State, getContext, and Tweened Stores with $runes

Ilja October 17, 2024
Source

Svelte version at time of writing: svelte@5.0.5

Ok so Svelte 5 has officially been launched (aka out of beta) a few days ago and the official migration guide followed shortly after. It was kind of a running joke among my peers that while in beta, the Svelte 5 documentation was a bit “sparse”. Like, only a few months ago we didn’t know for sure when to use $effect or how to use $derived when dynamically fetching data with async functions… Or how to replicate the ergonomics of file-based stores without using stores in Svelte 5.

Also, I ain’t gonna lie: I was rather shocked when $runes were first demo-ed with getters and setters and Class-based approaches and it took me some time to accept the new reality. I already started to miss the simple days of $: just_do_it_and_dont_make_me_think.

Turns out that a lot has happened since then and I want to share some of the simple / simplified patterns that I’m currently using in production (partly since May/June, partly only recently).

I’d say that Svelte 5 is actually easier and absolutely mightier than ever. One part of “mightier” is that it’s indeed far more predictable which was also part of the reasoning behind Svelte 5.

Anyhow: Let’s start.

Disclaimer: I collected some of the learnings and patterns here from trying to make SvelteKit 2 prerender and SSR with Svelte 5 as perfectly as (economically) reasonable for elections live coverage. One core challenge for using prerendering and SSR with live data is that you need to provide an initial state prerenderable to a set of components, and later on update them all frequently in a consistent and reliable matter.

On a Side-Note: Mixing Svelte 4 / 5

Actually totally do-able off the shelf. Even in SvelteKit. Close to zero effort.

Sometimes you might need some helpers such as fromStore (svelte/store) or $effect to update tweened store values (see below). But other than that, just use them together - esp. if you’re not in the position to fully migrate to Svelte 5 right now.

Bye bye Stores: Simple Shared State without Getters / Setters

Yes, getters / setters and Class (probably) totally have their uses.

It’s just that I’m only an ok-ish JavaScript developer and I totally got spoiled by Svelte 3’s simplicity. At least as of now (following an intense election-sprint-driven year), I can’t be bothered to write boilerplate-y code just to mimic what syntactic sugar around stores enabled me to do with 1-liners in the past:

import { store } from $stores/data;

// read let param = $store

// write $store = value

I just want reactive shared state.

Instead I ended up producing semi-valid slop like this, using a getter but not using setters:

// store.svelte.js const counter = () => { let _count = $state(0)

return { get count() { return _count }, increment() { _count += 1 }, decrement() { _count -= 1 }, reset(value = 0) { _count = value } } }

export const count = counter()

Well, I just don’t get it - at least for now.

Moving forward, the future of svelte/store also isn’t entirely clear, it seems. And working with them in Svelte 5 feels a bit awkward (because you might need helpers like fromStore(store)) and again it feels boilerplate-y.

Plus: Runes are cool. Runes are better (e.g. granular even on more complex Objects). The $derived.by(function) rune allows me to do stuff I didn’t even try to learn with the derived store.

So right now, I’m using two lazy lean minimalist patterns to share state between components and scopes:

file-based with .svelte.js universal reactivity (I continue to call them “stores”) context-based with setContext / getContext and 1 trick

File-based shared runed state with .svelte.js files a.k.a. “Svelte 5 Stores”

“readable”: simple read-only export

create the runed store

// data.svelte.js // in this case it doesn't matter if it's const or let export const store = $state(13)

import and consume in component

// Component.svelte

value: {store}

💡 trying to write to / update / assign to the “store” will throw an error:

Cannot assign to import

“writable”: export an Object

create the runed store

// data.svelte.js export const store = $state({value: 13})

import and consume in component

// Component.svelte

value: {store.value}

update / write

store.value = new_value

update / write by invoking a closure (incl. store.value++ and so on)

<button onclick={() => (store.value = new_value)}>++

you can actually somewhat conveniently add mock methods (or dare I say: “setters”), by adding self-referential closures to the state object

export const store = $state({ value: 13, increase: () => store.count++ })

you can now invoke the function / “method” / “setter”

++

However, notice how the closure explicitly references the exported object, as this-based approaches don’t seem to work (yes, I’ve read MDN on this). If you know how to make this work without rocket science (or getters / setters), please let me know! Is it something about Proxy?

export const store = $state({ value: 13, increase: () => store.count++,

// doesn't work; logs the invoking element decrease() { console.log(this) this.value-- },

// doesn't work; logs undefined (as can be expected) update: () => { console.log(this) this.value-- },

// doesn't work; logs the invoking element test: function () { console.log(this) this.value-- } })

Works for me and is easy to memorize.

We’ll see how that will turn out in future versions of Svelte. Also, I must admit that I haven’t tested complex / nested Objects and the impact of this approach on the granularity of the reactivity. If I learn something new, I will update this post.

Check out the example REPL.

💡 I will write about Class-based runed state sharing or using getters / setters when I’m more comfortable with them.

Final note: I actually put the .svelte.js files (can we just continue to call them stores?) in the same folder where I used to put Svelte 3/4 stores: src/lib/stores aliased as $stores, $state or $data.

Shared runed state via getContext / setContext (and Closures)

You’re probably familiar with that pattern: sharing reactive state between related (!) components without a file-based store, or without passing and binding props three levels deep.

There’s actually quite a good use case for this pattern which involves the Singleton-nature of ES/UMD-modules and mounting the same compiled (!) module multiple times within the same window/document - but that’s for another blog post.

Old getContext / setContext Pattern with Svelte 3/4

set the context in a parent / higher-level component

// Parent.svelte

get the context in a related (as in a family tree) component consume as a store ($store = value etc.)

// Deeply_nested_child.svelte

store: {$store}

Worked perfectly fine but will throw / used to throw (?) errors in Svelte:

State referenced in its own scope will never update. Did you mean to reference it inside a closure?

Turns out, there is a set of approaches to make this pattern work again with Svelte.

New Svelte 5 setContext / getContext Pattern

💡 Please note, that in mixed Svelte 4 / 5 mode the child can remain in Svelte 4 mode and consume the store with the Svelte 3 / 4 approach (see REPL). In pure Svelte 5 mode, it’ll throw an error

store is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates (non_reactive_update)

“readable”: simple read-only context

similar to the Svelte 3 /4 approach, define your runed store / $state in the upper parent scope note how the context is set as a closure

consume in Child /deeper nested component by invoking the closure (!)

value: {store()}

“writable”: export an Object

very similar to the file-based store approach, define the state as an Object, so that it gets proxied here, we don’t need a closure as we pass the proxied Object (by reference, I guess)

to consume, just read / access the value

value: {store.value}

💡 you can add methods / handlers in the same way as we did for the file-based runed stores

Check out the example REPL.

update (2024-10-25): $derived works as well, using the closure approach:

const derived_store = $derived(readable * writable.value); setContext("derived_store", () => derived_store)

Local State: $derived reconciliation

assume you have a component with an initial value (e.g. passed as $props()) e.g. for prerendering or to provide a state before the user interacts later the value is going to change, coming from a different place (such as a centralized data polling function in a shared store, a user action and so on) we’re gonna use $derived.by with a reconciliation if-else check to pick the value to be displayed Empty state in store:

// state.svelte.js export const external_state = $state({value: null})

reconciliation logic in component

See the approach in action in the example REPL.

Svelte 5, Runes and Tweened Stores

so let’s say you have a tweened store for a numeric value you want to tween when some runed states updates pnpx sv migrate svelte-5 resulted in some run() from svelte/legacy package which just didn’t look right Instead, I suggest to use $effect` to update the tweened store value.

// Component.svelte

See example REPL with a slider.

That’s it for this part. I hope this is helpful to others. My next posts might be about proper SvelteKit prerendering or using Storybook with SvelteKit in library / package mode.

Let me know what you think or correct me where I’m wrong:

Email: contact ( at ) fubits (dot) dev Bluesky: https://bsky.app/profile/fubits.dev Twitter (duh): https://x.com/fubits

Bye.

Discussion in the ATmosphere

Loading comments...