DEV Community

Brendan Matkin
Brendan Matkin

Posted on

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 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);
Enter fullscreen mode Exit fullscreen mode

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

Top comments (7)

Collapse
 
brendanmatkin profile image
Brendan Matkin

Also check out @jdgamble555 's store series. In article 2 they suggest a neat workaround! dev.to/jdgamble555/series/22831

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Yup, you explained the context necessity, I just tried to avoid it lol

Collapse
 
brendanmatkin profile image
Brendan Matkin

Haha I really didn't want to do it! It felt like I was doing something wrong to have to do a workaround like this. Oh well at least it works for now

Collapse
 
jimnayzium profile image
Jim Nayzium

Thanks so much for this post! I am hobbyist old timer PHP and Macromedia Flash action script 2.0 from back in the day guy, who has always been a part timer. I am jealous of you guys who are so knowledgable and talented! SvelteKit has been the first framework that I feel like was developed the exact way my brain operates. ๐Ÿ˜‚ however, recently I have been terribly confused by stores. And I think you are 100 percent right.

The UDEMY courses I did never differentiated between stores on the server side of things, and it was through mountains of trial and error I finally started to realize why the stores were or weren't available or were being shared on the public side! I am so used to handling all the $_SESSION stuff in PHP completely separated from the javascript that it truly gets confusing to me when it all bleeds together.

That being said, SvelteKit is so amazing overall, I am in love with it! And posts like this just help me thrust forward into more understanding, so thanks!

Collapse
 
jimnayzium profile image
Jim Nayzium

And a quick follow up question. If I do have a userStore that I want to utilize in multiple places easily as you describe, I avoid ever having it in stores.js and only declare it initially in the top most .svelte file, in my case +layout.svelte as you've done here also, correct?

Meaning there is not some magical thing I will ruin by removing it from stores.js right? I assume stores can be declared and used anywhere, and the setContext is just a way to make it available to multiple places conveniently and securely for us.

Thanks again.

Collapse
 
yostar profile image
Yoav Schwartz

Jim, Iโ€™m sharing the same struggle where I canโ€™t wrap my head around how a simple php session variable translates to the world of sveltekit.

For example, if on login, a userโ€™s data is fetched from the database, where can i store it server side that will survive a page refresh or even just another request without having to fetch it from the database again?

I need it server side because subsequent api routes that write to the database will need access to the same data thatโ€™s also available front end.

Hoping you can unblock my confusion? Thx :)

Collapse
 
alexvdvalk profile image
Alex van der Valk

is there an easy way to test this out? I've tried locally as well as on vercel but wasn't able to reproduce this.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.