fubits.dev fubits.dev

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

SvelteSvelte5
I’ve been using Svelte 5 since mid May and learned some smart Svelte 5 patterns - some of which don’t even appear in the docs (at least as of today). Here are some of them.

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.

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

2. 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
<script>
	import { store } from '$stores/data.svelte.js'
</script>

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
<script>
	import { store } from '$stores/data.svelte.js'
</script>

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)}>++</button>
  • 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”
<button onclick={count.increase}>++</button>
  • 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

<script>
	import { writable } from "svelte/store"
	import { setContext } from "svelte"
	import Child from "./Child.svelte"
	
	const store = writable(data)
	setContext("store", store)
</script>
  • get the context in a related (as in a family tree) component
  • consume as a store ($store = value etc.)
// Deeply_nested_child.svelte
<script>
	import { getContext } from "svelte"
	const store = getContext("store")
</script>

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
<script>
	let data = $state(12)
	import { setContext } from "svelte"

	setContext("store", () => data)
</script>
  • consume in Child /deeper nested component by invoking the closure (!)
<script>
	import { getContext } from "svelte"
	let store = getContext("store")
</script>

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)
<script>
	const data = $state({value: 12})
	import { setContext } from "svelte"

	setContext("data ", data )
</script>
  • to consume, just read / access the value
<script>
	import { getContext } from "svelte"
	let store = getContext("store")
</script>

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)

3. 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
<script>
	import {external_state} from "./state.svelte.js"
		
		const initial_value = 12
		
		const local_state = $derived.by(() => {
			/** caution: 0 is falsy, value could be a Promise and so on...
			  * this check might need more sophistication depending in the context
				*/
			if (external_state.value) {
				return external_state.value
			} else {
				return initial_value
			}
		})
</script>

See the approach in action in the example REPL.

4. 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
<script>
	import {tweened} from "svelte/motion"

	const tweened_value = tweened(0)

	let input_state = $state(3)

	$effect(() => {
		// tweened_value.set(input_state)
		$tweened_value = input_state
	})
</script>

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:

Bye.