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