Closed konstantinblaesi closed 1 year ago
This is expected behaviour. Your store is created once on the server, when it starts up; if two users load pages that reference that store, they will both read the same value. In the browser, the store is created again when the app boots up, but with the initial value.
It's not a SvelteKit thing so much as an inevitable consequence of the fact that servers serve multiple users. If you want a store that is only used by a single user, your best bet is to return it from a universal load
function:
// src/routes/+layout.js
export function load() {
return {
value: writable(0)
};
}
<!-- inside a component -->
<script>
import { page } from '$app/stores';
$: ({ value } = $page.data); // or `$: value = $page.data.value`, if you prefer
</script>
<h1>value is {$value}</h1>
For good measure, define App.PageData
so that you get type safety:
// src/app.d.ts
import { Writable } from 'svelte/store';
declare global {
namespace App {
interface PageData {
value: Writable<number>;
}
}
}
export {};
Will leave this open with a documentation
label.
Would this go under https://kit.svelte.dev/docs/load#shared-state ? Is there anything else there we need to say, or do we just need to make it stand out more?
Stuff like this makes it far too easy to create security holes, ideally the docs should have it in big flashing lights :rofl:
I think with global stores being such a common pattern in vanilla Svelte (even being used in the tutorial extensively) we need to make this behavior (and suggested solutions) more prominent -- it's a real paradigm shift for people. I bet many people who read that docs section don't realize that "shared variables outside your load function" includes stores.
Not sure if https://github.com/sveltejs/kit/pull/8547 intersects with this at all.
I'm trying to write some docs for this now, but I'm struggling to come up with a plausible use case — I can think of examples of sensitive non-store information that would be returned from a load
(username etc), and examples of stores that you'd set outside the context of a load
(albeit only in the browser, e.g. a volume
setting for media elements) but nothing that falls into both categories. What's an example that people have come upon in the wild?
I think the count
example is actually a good one because it's usually the first store everyone makes when learning, and it's ambiguous enough to represent "any store with global state".
Considering that --
I imagine this is going to be a widespread and dangerous footgun for many users. I've been using Svelte daily for years and I still forget about this sometimes (albiet my habits likely contribute to my forgetfulness).
Are we certain that documentation warning people is the best/only solution? Just spitballing some (mostly unrealistic) thoughts I've had:
Chiming in here, I have shot myself in the feet with this on a current project
come up with a plausible use case
In my use case, I have a glorified todo list app. The current user's particular todos are loaded into a store so they can be modified post-load. I realize that the store should be initialized in the load()
now, but previously I was just loading the data itself in load()
and then passing it to a global store's initializer and then using it directly in a bunch of child components... because I wasn't aware of the behavior.
I too am a bit worried about accidentally re-introducing other shared stores accidentally in the future though.
I have a hard time even thinking of a good use case for having a globally shared server-side store... the power of stores IMO is their connection to the svelte components, reactivity, etc, which isn't the case in SSR. If I wanted to notify something server side of changes to a value I could just use observable / EventEmitter / queues etc ... and that feels like a less common need than "ship some reactive data to the frontend after rendering it on the backend for a request"
I'd say the OP's case is an excellent example on how HMR works and should work - but maybe I am overlooking somnething given the comments by others..
I'm trying to write some docs for this now, but I'm struggling to come up with a plausible use case — I can think of examples of sensitive non-store information that would be returned from a
load
(username etc), and examples of stores that you'd set outside the context of aload
(albeit only in the browser, e.g. avolume
setting for media elements) but nothing that falls into both categories. What's an example that people have come upon in the wild?
FYI. This is what I was dealing with originally.
Thanks for the examples @fzembow and @hshoob. This is what I suspected — the problem isn't the stores, it's the way they're being used. (This isn't a criticism! We need to find a way to make what I'm about to say more obvious.)
Your load
functions should be pure — no side-effects. That means no setting store values, no writing to databases, no modifying any variables outside the load
function. You can bend the rules for the sake of observability (e.g. console.log(stuff)
or updating a last_viewed
field in a database are harmless), but beyond that it's a recipe for confusion. Does the user object need to be a store, or could it be accessed via $page.data.user
instead?
The same goes for the top level of a component's <script>
block — writing to $strokeWidth
is a side-effect that has impact beyond the scope of the component itself (a store that doesn't belong to the component has a value that depends on whether or not the component happens to be rendered which is a bad situation to be in).
A page I link to a lot is CommandQuerySeparation. In essence, the idea is that a function should return something or it should do something, but never both. That very much applies here. (If I were designing a language, I'd enforce this at the syntax level!) There are occasional exceptions (in the linked page, Fowler mentions popping a stack; logging is another obvious example) but they are exactly that — exceptions.
So while there are legitimate reasons to return a store from a load
(e.g. realtime subscriptions, where something like stock price data can change without a navigation), I don't know that documenting this pattern as a way of dealing with global state is the right approach. It feels like we need to do a better job of communicating that your load
and <script>
blocks shouldn't have side-effects. Global stores are totally fine, as long as you don't write to them during data loading or render.
One possibility that springs to mind, inspired by @FractalHQ's suggestions — perhaps we could alias svelte/store
to a wrapper module that does something like this:
import { writable, readable, derived, get } from 'svelte/store'; // not sure how to prevent this from being circular
import { BROWSER } from 'esm-env';
function safe_writable(value, start) {
const store = writable(value, start);
if (!BROWSER) {
store.set = store.update = () => {
throw new Error('Cannot write to a store on the server');
};
}
return store;
}
export { readable, derived, get, safe_writable as writable };
@dummdidumm pointed out that that's not really a workable solution, since valid things like this would be prevented:
<script>
import { spring } from 'svelte/motion';
export let count = 0;
const value = spring();
$: $value = count;
</script>
Unless we tried something sneakier, like allowing a .set(value)
if the current value is undefined
, or allowing the first .set(...)
but not the second?
wait... this is easy. we just need to differentiate between stores that were created during render and those that weren't:
import { writable, readable, derived, get } from 'svelte/store'; // not sure how to prevent this from being circular
import { BROWSER } from 'esm-env';
function safe_writable(value, start) {
const store = writable(value, start);
+ if (!BROWSER && !IS_LOADING_OR_RENDERING) {
store.set = store.update = () => {
throw new Error('Cannot write to a store on the server');
};
}
return store;
}
export { readable, derived, get, safe_writable as writable };
Won't that also throw for your spring
example above, since it's calling set
during rendering in the reactive statement? AFAIK we would still want that to be allowed since the store isn't global, it's created in the component.
Could you check for a component context on initialisation?
Won't that also throw for your
spring
example above, since it's callingset
during rendering in the reactive statement?
In that case, IS_LOADING_OR_RENDERING
would be true
, so the monkey-patching wouldn't happen. Pseudo-code:
// src/runtime/server/page/load_data.js
IS_LOADING_OR_RENDERING = true;
const result = await node.universal.load.call(null, {...});
IS_LOADING_OR_RENDERING = false;
// src/runtime/server/page/render.js
IS_LOADING_OR_RENDERING = true;
rendered = options.root.render(props);
IS_LOADING_OR_RENDERING = false;
(Though now that I look at that code, I'm not sure how to deal with the possibility that an illegal set happens while an unrelated load
is happening... though those cases would be extremely rare since this would only happen in dev, where the server only has one user to worry about. We really need AsyncLocalStorage to get standardised.)
Checking for a component context would work for the rendering bit, yeah, just not the loading bit.
On some reflection, I think I came to a conclusion as to why I keep getting tripped up on this, even though rationally I totally understand the current implementation's behavior. I think it's somewhat due to my mental positioning and primary usage of svelte as a "powerful prototyping tool": I usually start with a frontend-only perspective, with all state lost on a reload, and only later add meaningful server-side persistence to a project.
With this progression, it's really tempting to just take some existing global store and just stick the result of a load into it -- somewhat of a path-dependent developer experience :)
"If you want a store that is only used by a single user"
Does a store "being used by a single user" equate to a store that's client-side only?
Separately, i like the idea of making another type of store with a more specific/descriptive name.
Installing 3rd party store libraries to change the behavior of stores i had already made has been a very nice experience. I feel like some of those store types (e.g. local-storage-based and cookie-based) should be built in to sveltekit
Before using SvelteKit, I would have a store called me
that made the logged-in user's information available throughout the client-side app.
Now using SvelteKit, my instinct was to do the same so I have this:
// layout.server.ts
import { trpc } from '../lib/trpc/client'
import type { LayoutServerLoad } from './$types'
export const load = (async (req) => {
const me = trpc().me.get.query()
return {
me
}
}) satisfies LayoutServerLoad
// layout.svelte
<script lang="ts">
import type { LayoutData } from './$types'
import { me } from '$lib/core/stores'
export let data: LayoutData
if (data.me) {
me.set(data.me)
}
</script>
<div>
<slot />
</div>
That seems to be creating a global store with the last logged in user's information which is definitely not what I intended!
Does that mean that for any data that I want server-side rendered, I can't have it in a store because it will end up as a server global?
Closing as we now have documentation which explains how to do this stuff safely
@Rich-Harris I don't feel like the documentation is quite approachable enough for noobs like me. It's not clear to me how to convert SPA patterns into equivalent SSR patterns. For example, I like to create stores like this:
const messageStore = writable(await loadMessages());
export default {
subscribe: messageStore.subscribe,
postMessage: (tag: string, text: string) => {
// Code for posting a new message with optimistic updates
},
};
This feels nice and elegant for an SPA, but of course once I enable SSR, loadMessages()
is run on the server and cached for all users, which is terrible.
What's the best alternative pattern for achieving the same result with SSR? My first instinct would be to load the data in load
and inject it into the store, but the documentation explicitly says to not do that: https://kit.svelte.dev/docs/state-management#no-side-effects-in-load
I want my data in a store so I can subscribe to it and attach related functions to it, but the documentation doesn't mention how to do that.
FWIW, I usually follow the docs that you linked to load the data in +page.ts, pass it to the template via page.data, then initialize the store in the template and store it in the context.
// +page.js
export async function load({ fetch }) {
const response = await fetch('/api/user');
return {
user: await response.json()
}
}
// +page.svelte
<script>
export let data
const store = writable(data.user)
setContext('user', store)
</script>
Or if you need this in all sibling routes, move the load()
to +layout.js and the setContext()
to +layout.svelte.
@coryvirok Got it, that makes sense, thanks! Sounds like load()
in +layout.svelte
plus setContext()
is the SSR-compatible equivalent for global stores.
But I am a little surprised that the canonical way to load data into a store is so roundabout. Feels like there's a lot of room for an improved developer experience here.
Describe the bug
Steps:
Workaround:
Restart the dev server, the SSR rendered page no longer has value 1 but value 0
Question:
Is this intended/expected behavior, is it configurable or a bug in vite or svelte?
Reproduction
https://github.com/konstantinblaesi/store-issue-repro
Logs
No response
System Info
Severity
annoyance
Additional Information
Original discord discussion