Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-toes-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: print useful error when subscribing to SvelteKit's stores at the wrong time during SSR
21 changes: 18 additions & 3 deletions packages/kit/src/runtime/app/stores.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ export const getStores = () => {
export const page = {
/** @param {(value: any) => void} fn */
subscribe(fn) {
const store = getStores().page;
const store = __SVELTEKIT_DEV__ ? get_store('page') : getStores().page;
return store.subscribe(fn);
}
};

/** @type {typeof import('$app/stores').navigating} */
export const navigating = {
subscribe(fn) {
const store = getStores().navigating;
const store = __SVELTEKIT_DEV__ ? get_store('navigating') : getStores().navigating;
return store.subscribe(fn);
}
};

/** @type {typeof import('$app/stores').updated} */
export const updated = {
subscribe(fn) {
const store = getStores().updated;
const store = __SVELTEKIT_DEV__ ? get_store('updated') : getStores().updated;

if (browser) {
updated.check = store.check;
Expand All @@ -55,3 +55,18 @@ export const updated = {
);
}
};

/**
* @template {keyof ReturnType<typeof getStores>} Name
* @param {Name} name
* @returns {ReturnType<typeof getStores>[Name]}
*/
function get_store(name) {
try {
return getStores()[name];
} catch (e) {
throw new Error(
`Cannot subscribe to '${name}' store on the server outside of a Svelte component, as it is bound to the current request via component context. This prevents state from leaking between users.`
);
}
}
8 changes: 7 additions & 1 deletion packages/kit/types/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,22 @@ declare module '$app/stores' {

/**
* A readable store whose value contains page data.
*
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
*/
export const page: Readable<Page>;
/**
* A readable store.
* When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties.
* When navigating finishes, its value reverts to `null`.
*
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
*/
export const navigating: Readable<Navigation | null>;
/**
* A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
* A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
*
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time.
*/
export const updated: Readable<boolean> & { check(): Promise<boolean> };

Expand Down