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.