DEV Community

Cover image for The Correct Way to Use Stores in SvelteKit
Jonathan Gamble
Jonathan Gamble

Posted on • Updated on

The Correct Way to Use Stores in SvelteKit

Svelte Stores are amazing. They allow you to share data across different components, and keep them reactive. Everything is done automatically. This is completely unique from every other framework. There are no awkward providers that are parents of providers. Things just work with the Svelte Magic. Some people hate this, I personally love it.

That being said, this unfortunately is not the case in SvelteKit. The current prediction of the future of the web will need server components. SvelteKit, not Svelte, especially now that it is in 1.0, will be able to handle it. However, most tutorials are written for Svelte; I suspect we shall see a lot of poorly written code using stores in SvelteKit. Let's address the problem and how to fix it.

1. Do not use stores in endpoints, actions, or load functions

Stores will not work correctly on the server as is, so just don't use them on the server. You could technically make an exception for a load function where you check for the browser environment, but in reality, if you have the correct configuration, everything you need should be available in the $page variable. This will also prevent you from accidently declaring a global variable. Just don't do it. There is always a better way.

2. Do not use stores at all in isolated components

Technically you can do this, but there is no reason to. There are so many tutorials that show examples of this, but you don't need to. If you're not sharing the data across components, simple use the reactive declaration - $:. Declarative programming will keep you from having to manage subscriptions, and you don't need to worry about passing any props.

3. Do not pass a store as a parameter

This may seem controversial, but it makes sense if you think about it. If you're only using a store in a parent child component, see #2. Make the variables reactive or create an event dispatcher.

The Correct Way

According to Svelte Docs, the official way to handle this is to use setContext and getContext. However, we want to follow best practices, specifically the single responsibility principle. Any senior React engineer will know to separate hooks into their own component. Each store should follow the same principles.

First, create a reusable useReadable and useWritable component. This will handle the contexts for you so that you don't have to think about it.

use-shared-store.ts

import { getContext, hasContext, setContext } from "svelte";
import { readable, writable } from "svelte/store";

// context for any type of store
export const useSharedStore = <T, A>(
    name: string,
    fn: (value?: A) => T,
    defaultValue?: A,
) => {
    if (hasContext(name)) {
        return getContext<T>(name);
    }
    const _value = fn(defaultValue);
    setContext(name, _value);
    return _value;
};

// writable store context
export const useWritable = <T>(name: string, value: T) =>
    useSharedStore(name, writable, value);

// readable store context
export const useReadable = <T>(name: string, value: T) =>
    useSharedStore(name, readable, value);
Enter fullscreen mode Exit fullscreen mode

Next, create your stores just like you would create a custom writable. The only difference is that you have to name your store with a string. Here we use dark.

stores.ts

export const useDarkMode = () => useWritable('dark', false);
Enter fullscreen mode Exit fullscreen mode

And finally, import the hook to use your store in your component. Then you can use it just like any store.

<script lang="ts">
  import { useDarkMode } from 'stores.ts';
  ...
  const darkMode = useDarkMode();
</script>

{#if $darkMode}
  // do something
{/if}
Enter fullscreen mode Exit fullscreen mode

The beauty of this is that that you don't have to think about context at all; you just import your hook.

And Custom Stores... ?

Follow my previous post for custom stores. Here you can just use it like any other type of store.

export const jokerStore = (value: string) = {
  const { set, update, subscribe } = writable<string | null>(value);
  return {
    set,
    update,
    subscribe
    setJoker: () => set('joker')
  }
};

export const useJoker = () =>
    useSharedStore('joker-store', jokerStore, 'harley');
Enter fullscreen mode Exit fullscreen mode

Siblings

I should add that if you share a store between two sibling components, you will have to declare the hook in the parent component. You can simply do:

useMyHook();
Enter fullscreen mode Exit fullscreen mode

This will set the context automatically under-the-hood to a higher level for shareability.

Final Thoughts

I am a firm believer that this should be built into SvelteKit and we should not have to think about it. There will unfortunately be a lot of data leaks because of coders not being aware that a global variable can be shared. Svelte Team, Please make this happen! There are a handle full of issues regarding this. There are also over-complicated work-arounds and external packages (I kind of like the Map version), but... as a Mandalorian will tell you.

This is the way...

Check out code.build for more tips. I am currently rebuilding it with Tailwind...

J

Top comments (7)

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Hi Joe. What exactly is "awful?" I would be glad to update something if it is unclear. The SvelteKit docs just say to use context, but don't mention the gotchas that I do etc.

Collapse
 
jrmoynihan profile image
jrmoynihan • Edited

Your point about not needing global state when local will do is great.

That being sad, your point in #2 has some exceptions. Isolated components might need to make use of certain stores, like tweened and spring because they already provide specific behaviors out of the box.

I also like the idea to use stores + contexts, but I found this implementation a little hard to follow. Part of it is probably the nomenclature you used in the post. These aren't "hooks", at least in Svelte's definition of them, even though they provide similar functionality to React hooks. The "useWritable" semantics also feel weird -- if I wanted to use writable(), I'd just import it! What does "use" tell you about what the function does?

As a comparison, I wrote a similar type of convenience function for stores + contexts a while back with some more explicit typing, even for the generics, but I also stuck with the "get/set" naming convention Svelte uses internally for contexts. I think this makes it much more clear to the end-user what is actually being done here. You could still make a getDarkMode() function if you were doing that context lookup a lot and wanted a quick alias for getWritableContext('dark').

One of the problems with context that Rich brings up in the tutorial, is using unique keys within a context tree. I don't think your implementation can deal with that as-written. Symbol can solve that, as he points out.

Lastly, I'm curious why you chose to put the getContext() and setContext() within the same function, useSharedStore? That feels a little risky to me. Isn't it important to know that a context a child component is trying to get wasn't set above in its parent tree? The child component shouldn't be doing the setting here if the provided key fails the if(hasContext(name)) check!

I don't always get the single-responsibility principle correct in my own code either, but this seems like it would be substantially improved just by being more explicit, and splitting up the functionality.

Collapse
 
jdgamble555 profile image
Jonathan Gamble

What is tweened and sprint? I think I agree there are exceptions, but generally people could avoid stores for reactive statements.

Collapse
 
brendanmatkin profile image
Brendan Matkin

This is an interesting solution! I agree that something like this should just be built in. Gonna try this soon!

I also just wrote something up that's a little higher level explanation of what's going on. I don't have any novel fixes, just some clarification and summary of what's missing from the docs: dev.to/brendanmatkin/safe-svelteki...

Collapse
 
terris-linenbach profile image
Terris Linenbach • Edited

use is used by Svelte actions, so the prefix use doesn't look like a React hook in Svelte code, which would only look familiar to React developers. Use plain get instead if you need a prefix. Otherwise, there are good points here. I'm stealing useSharedStore (with a different name..)

Collapse
 
alanxtreme profile image
Alan Daniel

I'd like to know what are your throughts about signals, because i'd hate to write a lot of update(self => x = y, z = 1, return self) every time i just want to update a parameter.. I'm considering on waiting for svelte 5

Collapse
 
otherjohn profile image
John Hamman

How would you use "derived" with all this?