SvelteKit Contexts and Stores
We’ve launched coin.fun on Solana Mainnet a few weeks ago. Despite my earlier conclusion that “Web3 tooling isn’t ready for frameworks beyond React”, we stubbornly built coin.fun in SvelteKit. The tl;dr is that developing in Svelte has been a joy, but there were some framework-specific things that raised some confusion. One of these being context and stores.
Contexts and stores, on their face, seem pretty straightforward. Context in Svelte, just like context in React, is a method to share data in a component tree. Svelte stores are the primary mechanism to share reactive state across many components.
Okay, cool.
Wait. They sound pretty similar.
I guess, in short, they’re both concerned with sharing things across component trees. Key difference being that stores are concerned with reactive state, whereas context can be used to share anything: static values, functions, reactive state, anything.
Unfortunately, the Svelte and SvelteKit docs fall a bit short when it comes to the intricacies of context. I guess it’s more of a lack of idiomatic patterns than necessarily omission of critical concepts. Let’s just say some things are left to imagination.
The docs on context do mention a few times that getContext
and setContext
“must be called during component instantiation.” Furthermore, SvelteKit offers a
little insight how context can be utilized to isolate store instances per
client[1].
Anyway. Lets engage in the pleasure of finding things out.
Context Playground
There’s a small context playground I created in the Svelte REPL. It’s no SvelteKit so we can’t check some SSR-specific things—like how SvelteKit reuses component instances across navigations—but that’s something we can sort of emulate.
I encourage you to play around a bit.
The playground here forgoes idiomatic usage of context. Especially the use of
reactive getContext
and setContext
calls is something which would make Rich
shudder, I imagine. It’s a good showcase of context anti-patterns—I will circle
back to this later—and that’s why it’s educational.
Setup
The playground has three sets of fields.
- A context key, which you can change to check how dynamic keys work.
- Two context values, as a reactive binding or a store value.
- Toggles for triggering render and mounts of children.
The component utilizes <svelte:self />
to recursively render itself, which
allows you to check behavior of components consuming a context set by a parent.
Observations
Now, when playing with just the two values we can set things look fine and dandy. We can see that our value binding updates, and our store binding updates values in nested components consuming context.
It appears “component instantiation” is a bit of a “loose” concept though. I
cheekily added some getContext()
calls inside the HTML template (don’t do this
btw). So if you toggle the context
flag, observe how the values are updated
when the #if
block (re)renders it’s contents. It makes sense though, because
the template is just as much instantiated as the code in our <script>
.
The nested component carrying over the same context key behaves as expected as
well. The getContext()
calls inside our nested component carry over the store
value but do not carry over the input value. And, just as with toggling the
context
flag, toggling the nested
flag on our root will instantiate the
child component and thus propagate the bound input value.
If we add another nested component (toggle nested
on in the first child), you
would notice that our values appeared to have stopped propagating. This is not a
bug but 💯 expected because of how Svelte context works. Svelte’s setContext()
attaches a key/value context to the component tree. As we are calling
setContext()
in every component instance, the grandchild will inherit the
context set by the first child, which is undefined
. Note that calling order
is of importance as well. Because we call getContext()
before setContext()
,
we grab the first context instance we find when walking up the component tree;
not the instance attached to the current component.
However…
Things change when we take the context key
into account. Notice how, in the
playground, I’m calling $: setContext(key)
reactively (also: don’t do this),
meaning it’s called on to changes to key
. Also note we’re grabbing context
reactively on changes to key
with $: ctxReactive = getContext(key)
(again:
doing this is not advisable). Anyway.
Here we discover why setting and getting context reactively is a bad idea. When
you play around with changing context key on both the root element and the
child, you’ll see that the const getContext()
exhibits the most consistent
and, in my view most expected, behavior. Now, this is probably because the
playground is deliberate exceptionally bad implementation of utilizing context,
but it perfectly shows why you’d want to approach setting and getting context as
something static.
A digression into React
Now, Svelte’s context model is quite a bit different than that of React. React
is pretty explicit in how it deals with context. You need a Provider
component
which explicitly wraps a component sub-tree. Components in that sub-tree can
consume the Provider
’s value. This value is inherently reactive in React
because it’s just a prop on Provider
. Also, if the provider value is a struct,
you can easily hook up parts into React’s reactivity by utilizing useState()
or useReducer()
.
In Svelte context, through setContext()
is directly bound to the current
component sub-tree. When you getContext()
before setting it, you can get
access to the parent’s context. The context value also isn’t something
inherently reactive as opposed to React’s model.
Although you could hook up getting/setting context up to Svelte’s reactivity, by
making key
reactive, the API clearly isn’t built for this scenario. An obvious
hint here, is that Svelte throws whenever you call getContext()
or
setContext()
in an event handler. Now, I’m not sure whether allowing calls in
reactive statements is an oversight or possibly difficult to detect or a valid
use-case for very specific scenarios, but generally it looks like doing this is
a recipe for subtle bugs.
SvelteKit stores in context
So far I’ve mainly written about what not to do. So what’s the proper way here?
If you need to pass reactive variables you will need to wrap a store inside a context. This way both the root and descendants of a sub-tree can consume and/or update the store’s value contained within the context.
A helpful pattern I’ve been applying here is making sure stores have some sort of initializer method. This method we can hook up to Svelte’s reactivity system so it may propagate changes to descendants of the tree which consume its context. The basic patter is like so:
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
export let someValue;
const store = writable(someValue);
setContext('key', store);
// Make sure store updates when `someValue` changes.
$: $store = someValue;
</script>
This example is a bit contrived though. In coin.fun we have some pretty gnarly
stores keeping track of signed in users, onchain operations, and other more
complex data. In some cases we also need to set up listeners to websocket
events, or kick off a fetch()
. And in many cases we need multiple descendant
components of a sub-tree consume these stores. We roughly apply the following
pattern in a file (or files, depending on complexity):
// store.js
import { setContext } from "svelte";
import { writable } from "svelte/store";
const KEY = "myContext";
function createStore(initialValue) {
const { set, subscribe } = writable(initialValue);
function init(nextValue) {
// Here you could place some initialization logic.
// Afterwards, update the store with `nextValue`.
set(nextValue);
}
return {
init,
subscribe,
};
}
export function getValueContext() {
// If you're antsy about children being able to initialize
// store values, you can do something like the following to
// omit the `init()` method:
//
// const { init: _, ...store } = getContext(KEY);
// return store;
//
return getContext(KEY);
}
export function setValueContext(initialValue) {
const store = createStore(initialValue);
setContext(KEY, store);
return store;
}
This can then be set and consumed in a parent, a route file for example, like so:
<!-- +page.svelte -->
<script>
import { setValueContext } from './store.js';
export let data; // page data as received from load()
// For initial, full-page loads we create the store and set an
// initial value.
const store = setValueContext(data.value);
// Because SvelteKit reuses components and does not re-mount
// we can update the corresponding store reactively whenever
// `data.value` changes due to navigation.
$: store.init(data.value);
</script>
Having an init()
method also helps if you need/want to be explicit in
initializing the store value on certain lifecycle events, like onMount()
,
afterNavigate()
, etc.
Svelte’s Magic
Building coin.fun in SvelteKit was an absolute joy. We did struggle a tiny bit learning the intricacies of Svelte. Reactivity in Svelte 4 is pretty magical at times, and can definitely bite you if you’re dealing with a lot of reactive variables dependent on each other. The context playground being a good case-in-point, as we’re definitely doing some naughty things there which anyone unfamiliar with the behavior could try. One of the things in Svelte 5 I’m looking forward to is it requiring you to be a lot more explicit when it comes to reactivity.
I hope this post provides some insights in how to apply context in Svelte and SvelteKit. Most of all, I hope it at least explains a little bit how context behaves with store-in-context scenarios, and why you’d want to adhere to Svelte and SvelteKit’s documentation.
🖖
Let me know if this blows. Reply on X.
The SvelteKit docs on state management are very adamant about not relying on global, ex. module-scoped, state because this could leak state to other clients. They mention:
“You might wonder how we’re able to use
$page.data
and other app stores if we can’t use our own stores. The answer is that app stores on the server use Svelte’s context API — the store is attached to the component tree withsetContext
, and when you subscribe you retrieve it withgetContext
.”Well, what I’m wondering still is how they’re doing this and making
$page
available through a module import because it feels a lot less cumbersome than importing and calling agetContext
before being able to consume the store. ↩︎