Safe SvelteKit Stores for SSR

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 store…


This content originally appeared on DEV Community and was authored by Brendan Matkin

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:

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

  1. 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 picked src/routes/(dashboard)/+layout.svelte to keep it in scope of the auth'd stuff. Here I'll just use src/routes/+layout.svelte.

  2. 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>
    
  3. 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 a Symbol() and pass it around. See the Svelte tutorial for an example of that

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


This content originally appeared on DEV Community and was authored by Brendan Matkin


Print Share Comment Cite Upload Translate Updates
APA

Brendan Matkin | Sciencx (2023-05-08T20:24:35+00:00) Safe SvelteKit Stores for SSR. Retrieved from https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/

MLA
" » Safe SvelteKit Stores for SSR." Brendan Matkin | Sciencx - Monday May 8, 2023, https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/
HARVARD
Brendan Matkin | Sciencx Monday May 8, 2023 » Safe SvelteKit Stores for SSR., viewed ,<https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/>
VANCOUVER
Brendan Matkin | Sciencx - » Safe SvelteKit Stores for SSR. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/
CHICAGO
" » Safe SvelteKit Stores for SSR." Brendan Matkin | Sciencx - Accessed . https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/
IEEE
" » Safe SvelteKit Stores for SSR." Brendan Matkin | Sciencx [Online]. Available: https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/. [Accessed: ]
rf:citation
» Safe SvelteKit Stores for SSR | Brendan Matkin | Sciencx | https://www.scien.cx/2023/05/08/safe-sveltekit-stores-for-ssr/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.