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);
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);
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}
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');
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();
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 (9)
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.
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
andspring
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 forgetWritableContext('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()
andsetContext()
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 theif(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.
What is
tweened
andsprint
? I think I agree there are exceptions, but generally people could avoid stores for reactive statements.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...
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
use
is used by Svelte actions, so the prefixuse
doesn't look like a React hook in Svelte code, which would only look familiar to React developers. Use plainget
instead if you need a prefix. Otherwise, there are good points here. I'm stealing useSharedStore (with a different name..)Nicee
How would you use "derived" with all this?
can you revisit this for Svelte 5.0?