Svelte 5 Patterns (Part 1): Simple Shared State, getContext, and Tweened Stores with $runes
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