Safe SvelteKit Stores for SSR
Brendan Matkin
Posted on May 8, 2023
One of my favourite things about Svelte is the simplicity of svelte/store
for state management - especially auto-subscribing to a writable store with the $
prefix. Stores make inter-component communication so clear and easy. However, using global stores the way they are frequently shown in the Svelte documentation can result in data leaking between clients in a SvelteKit application.
BTW: I don't explain how to use stores here. There are oodles of tutorials (edit: this is a good one) and the documentation is super clear.
๐ Global Store is Shared
Imagine my disappointment to discover that using stores the way I was used to can result in sensitive store data being shared between clients. It took a coincidence for me to notice this was even happening, and much more searching than I would've hoped to figure out why and what to do about it. Hopefully I can save a few people some hours of hunting. โฌ๏ธSkip to Solution
There has been a lot of debate (1, 2, 3) about the problem, how to make it work, and how to document it. Additionally, some of the changes between beta and 1.0 have muddied the waters. Regardless of those challenges, the docs still aren't very explicit about it. They do say "No Side-Effects in Load" and "Avoid Shared State on the Server", but they are a bit fuzzy on clarifying that this means YOU SHOULD NEVER USE GLOBAL STORES IN SVELTEKIT. There are a few exceptions but generally just don't do it.
๐ It's Technically Not a Bug
The Problem
It seems that the SvelteKit developer consensus is that there is no issue. Everything is working as intended. You should already know to "Avoid Shared State on the Server". They are probably right. But it's not easy to tell that is what's happening, and the consequences are serious enough and easy enough to miss, that I think it deserves further clarification.
โฅ๏ธ To be clear, I LOVE Svelte and SvelteKit and have nothing but respect and gratitude for the developer team. I just wish it wasn't so easy for me to make this particular mistake.
The real problem here has two parts:
- It isn't immediately obvious that anything is wrong. You are free to put stores wherever and however you want in your app and it will generally work. You may never even notice that the stores might be shared. If you aren't using SSR then they never will be. Worst case scenario, it only happens for a short time during SSR (probably only for a few milliseconds). But this can obviously result in some pretty serious security and privacy issues.
- Svelte taught us to do stores this way. They showed us a hundred times how awesome and easy Svelte Stores are and how super amazing auto-subscribing to writables is. We listened and we agreed and we cheered and we used the snot out of them! And I haven't seen a single place where SvelteKit has un-taught us to do it this way.
Why SSR Stores Are Shared
I'm simplifying some stuff here. Please let me know in the comments if anything isn't right ๐.
Before SvelteKit, I was using Nuxt+Vue2+VueX, where stores are written normally but instantiated per-client (via plugins). In the case of SvelteKit, stores in a global stores.js/ts
are only unique on the browser (during and/or after hydration). Let's use the writable
store as an example. When you make a writable store like:
// file called stores.js
export const myStore = writable(0);
..stores.js
is a module. The module is executed, and writable
returns an object with member functions that allow us to interact with our new store ({set, update, subscribe}
). Remember, global modules are only executed once, and their root variables and functions are shared between all references. In this case, it is executed whenever the server starts. The wonderful, lovely, clean, nearly-vanilla-js nature of Svelte bites us in the butt here. During it's first render (assuming SSR), a client instance is referring to the same store.js
module that executed at startup. That module is creating a shared state on the server that this, and subsequent clients are interacting with. It's only after the code is copied to the browser that it becomes unique to a given client.
๐ค What To Do
Avoid Stores (๐)
Of course, you can just not use stores. You can pass data between components with props and events. There is nothing wrong with this, but it's not really a solution. I'm going to assume you are already doing this where it's convenient, and you want to also use stores for a reason.
Page Data
If you only need to load some data when a page loads, look at Page Data. $page.data
is a per-client store built into SvelteKit that you populate with a load
function and access via a single line (export let data
) in your component. It's really cool and if it works for your structure, do it! Otherwise, use the Context.
Stores With Context
The context API provides a mechanism for components to 'talk' to each other without passing around data and functions as props, or dispatching lots of events.
Although the docs aren't clear about warning us about using global stores, they are super clear about how to implement the fix. I'll mostly just re-word what's in the docs to keep things in one place (I'm leaving out types to keep things simple).
Choose the highest component in the hierarchy of components that you want to be able to access the store. Probably a
+layout.svelte
file. For example, my latest app had auth on a group called(dashboard)
, so I pickedsrc/routes/(dashboard)/+layout.svelte
to keep it in scope of the auth'd stuff. Here I'll just usesrc/routes/+layout.svelte
.-
Make one or more stores in the script section of that layout page (don't export them). They can be any type of store.
<script> // in src/routes/+layout.svelte import { writable, derived } from "svelte" const viewSelections = writable({ thing1: "something", thing2: "something else", booleanThing: false }); const myDerivedStore = derived( viewSelections, $viewSelections => $viewSelections.thing2 ); // this one updates reactively from page data: export let data; const storeThatUpdates = writable(); $: storeThatUpdates.set(data.someNeatProperty); </script>
-
Add store(s) to the context (Svelte tutorial, SvelteKit docs).
<script> // in src/routes/+layout.svelte import { writable, derived } from "svelte" import { setContext } from "svelte"; // NEW! const viewSelections = writable({ thing1: "something", thing2: "something else", booleanThing: false }); const myDerivedStore = derived( viewSelections, $viewSelections => $viewSelections.thing2 ); // this one updates reactively from page data: export let data; const storeThatUpdates = writable(); $: storeThatUpdates.set(data.someNeatProperty); setContext("viewSelections", viewSelections); // NEW! setContext("myDerivedStore", myDerivedStore); // NEW! setContext("storeThatUpdates", storeThatUpdates); // NEW! </script>
โ ๏ธCAUTION: the context key (e.g.,
"viewSelections"
) must be unique! A better strategy would be to use aSymbol()
and pass it around. See the Svelte tutorial for an example of that -
Get stores from context in any child component that you need them, and use them like you would any store that you imported
<script> import { getContext } from "svelte"; // each of these is a store viewSelections = getContext("viewSelections"); myDerivedStore = getContext("myDerivedStore"); storeThatUpdates = getContext("storeThatUpdates"); </script> <h1>Derived store: {$storeThatUpdates}</h1> {#if $viewSelections.booleanThing} <p>{$myDerivedStore}</p> {:else} <p>{$viewSelections.thing1}</p> {/if}
โ๏ธ That's It
If you use the context api, the stores aren't created until the component is created, and they are explicitly limited to the context in which they were created. No more data leaks!
I understand that the devs want to keep the docs clear and concise, which is a great goal! But in this case, I hope they choose to clarify why the typical global store pattern doesn't work here, and to be very explicit about the consequences.
Posted on May 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.