-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SSR rendered page retains old store value in dev mode #8614
Comments
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 // 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 // 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 |
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 🤣 |
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 #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 |
I think the |
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
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 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.. |
FYI. This is what I was dealing with originally. Screen.Recording.2023-01-19.at.5.35.39.PM.mov |
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 The same goes for the top level of a component's 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 |
One possibility that springs to mind, inspired by @fractalhq's suggestions — perhaps we could alias 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 |
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 |
Could you check for a component context on initialisation? |
In that case, // 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 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 Now using SvelteKit, my instinct was to do the same so I have this:
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, What's the best alternative pattern for achieving the same result with SSR? My first instinct would be to load the data in 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 |
@coryvirok Got it, that makes sense, thanks! Sounds like 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
The text was updated successfully, but these errors were encountered: