Repository: sveltejs/kit (JavaScript)

Issue:
Ability to access `$app/stores` at any time in the browser
### Describe the problem

There is sometimes code that I know will only ever be run on the browser (for example, if it's only called from an event handler), and having to jump through the same hoops as I do on the server in order to associate that with the appropriate (and only) instances of the stores is annoying.

### Describe the proposed solution

Store values on the browser should live in a singleton module (or something similar) rather than in the Svelte context for the root component. `getStores()` should be able to be called at any time, not just during component initialization. If it wouldn't cause too much of a divergence between browser and server code, we could also remove the transparent calling of `getStores()` during the autosubscription to any of the individual stores.

### Alternatives considered

Right now, I'm using a fairly ugly workaround where I serialize the values I need to a `data-foo` attribute in my root layout, which I then read and deserialize elsewhere.

Another option presumably would be to have an `onMount` call in my root layout that sets the store(s) as a global on `window`.

### Importance

would make my life easier

### Additional Information

_No response_


Discussion:
Oh that's a nice idea. It could also help with https://github.com/sveltejs/kit/discussions/4339 - provide initial values on the server, and have the ability to use them on the client freely.
What I have right now is extremely hacky but it seems to work:

```diff
diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js
index c71e30f5..d64829b5 100644
--- a/packages/kit/src/core/sync/write_root.js
+++ b/packages/kit/src/core/sync/write_root.js
@@ -43,6 +43,7 @@ export function write_root(manifest_data, output) {
 			<!-- This file is generated by @sveltejs/kit — do not edit it! -->
 			<script>
 				import { setContext, afterUpdate, onMount } from 'svelte';
+				import { browser } from '$app/env';
 
 				// stores
 				export let stores;
@@ -52,7 +53,11 @@ export function write_root(manifest_data, output) {
 				${levels.map((l) => `export let data_${l} = null;`).join('\n\t\t\t\t')}
 				export let errors;
 
-				setContext('__svelte__', stores);
+				if (browser) {
+					window._sk_stores = stores;
+				} else {
+					setContext('__svelte__', stores);
+				}
 
 				$: stores.page.set(page);
 				afterUpdate(stores.page.notify);
diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js
index d98e7171..fb046e61 100644
--- a/packages/kit/src/runtime/app/stores.js
+++ b/packages/kit/src/runtime/app/stores.js
@@ -15,7 +15,7 @@ export function stores() {
  * @type {import('$app/stores').getStores}
  */
 export const getStores = () => {
-	const stores = getContext('__svelte__');
+	const stores = browser ? window._sk_stores : getContext('__svelte__');
 
 	return {
 		page: {
```

Presumably I should be sticking the stores in a module somewhere when in the browser rather than onto `window`. Is there any precedent for where this sort of thing should live? Because it needs to be accessible from the generated root component, it might not be possible for it to live somewhere that it is _impossible_ for users to access, but it should at least live somewhere they are discouraged from accessing. Do we want to introduce (undocumented) something like `$app/internal`?
Oh, I was just circling back to this, and presumably I can set the stores in `packages/kit/src/runtime/client/client.js` instead of in the `Root.svelte` component definition - and here I will have access to internal things like `singletons.js`. I'm going to give that a try.



Patch:
diff --git a/.changeset/gentle-years-leave.md b/.changeset/gentle-years-leave.md
new file mode 100644
index 000000000000..9b512b8dce33
--- /dev/null
+++ b/.changeset/gentle-years-leave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Allow `$app/stores` to be used from anywhere on the browser
diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js
index c71e30f5b9d4..0dd8536bdd37 100644
--- a/packages/kit/src/core/sync/write_root.js
+++ b/packages/kit/src/core/sync/write_root.js
@@ -43,6 +43,7 @@ export function write_root(manifest_data, output) {
 			<!-- This file is generated by @sveltejs/kit — do not edit it! -->
 			<script>
 				import { setContext, afterUpdate, onMount } from 'svelte';
+				import { browser } from '$app/env';
 
 				// stores
 				export let stores;
@@ -52,7 +53,9 @@ export function write_root(manifest_data, output) {
 				${levels.map((l) => `export let data_${l} = null;`).join('\n\t\t\t\t')}
 				export let errors;
 
-				setContext('__svelte__', stores);
+				if (!browser) {
+					setContext('__svelte__', stores);
+				}
 
 				$: stores.page.set(page);
 				afterUpdate(stores.page.notify);
diff --git a/packages/kit/src/runtime/app/stores.js b/packages/kit/src/runtime/app/stores.js
index d98e71715b12..def12cf73665 100644
--- a/packages/kit/src/runtime/app/stores.js
+++ b/packages/kit/src/runtime/app/stores.js
@@ -1,5 +1,6 @@
 import { getContext } from 'svelte';
 import { browser } from './env.js';
+import { stores as browser_stores } from '../client/singletons.js';
 
 // TODO remove this (for 1.0? after 1.0?)
 let warned = false;
@@ -15,7 +16,7 @@ export function stores() {
  * @type {import('$app/stores').getStores}
  */
 export const getStores = () => {
-	const stores = getContext('__svelte__');
+	const stores = browser ? browser_stores : getContext('__svelte__');
 
 	return {
 		page: {
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 806214c2f64a..d4e286b68739 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1,15 +1,7 @@
 import { onMount, tick } from 'svelte';
-import { writable } from 'svelte/store';
 import { normalize_error } from '../../utils/error.js';
 import { LoadURL, decode_params, normalize_path } from '../../utils/url.js';
-import {
-	create_updated_store,
-	find_anchor,
-	get_base_uri,
-	get_href,
-	notifiable_store,
-	scroll_state
-} from './utils.js';
+import { find_anchor, get_base_uri, get_href, scroll_state } from './utils.js';
 import { lock_fetch, unlock_fetch, initial_fetch, native_fetch } from './fetcher.js';
 import { parse } from './parse.js';
 import { error } from '../../index/index.js';
@@ -17,6 +9,7 @@ import { error } from '../../index/index.js';
 import Root from '__GENERATED__/root.svelte';
 import { nodes, dictionary, matchers } from '__GENERATED__/client-manifest.js';
 import { HttpError, Redirect } from '../../index/private.js';
+import { stores } from './singletons.js';
 
 const SCROLL_KEY = 'sveltekit:scroll';
 const INDEX_KEY = 'sveltekit:index';
@@ -59,13 +52,6 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {Array<((href: string) => boolean)>} */
 	const invalidated = [];
 
-	const stores = {
-		url: notifiable_store({}),
-		page: notifiable_store({}),
-		navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
-		updated: create_updated_store()
-	};
-
 	/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
 	const load_cache = {
 		id: null,
diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js
index 6cf3e8417bd6..f1ab3986cb6c 100644
--- a/packages/kit/src/runtime/client/singletons.js
+++ b/packages/kit/src/runtime/client/singletons.js
@@ -1,3 +1,6 @@
+import { writable } from 'svelte/store';
+import { create_updated_store, notifiable_store } from './utils.js';
+
 /** @type {import('./types').Client} */
 export let client;
 
@@ -9,3 +12,10 @@ export let client;
 export function init(opts) {
 	client = opts.client;
 }
+
+export const stores = {
+	url: notifiable_store({}),
+	page: notifiable_store({}),
+	navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
+	updated: create_updated_store()
+};
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 25981f86bb25..678de3ebe48d 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -216,24 +216,16 @@ declare module '$app/paths' {
  * import { getStores, navigating, page, updated } from '$app/stores';
  * ```
  *
- * Stores are _contextual_ — they are added to the [context](https://svelte.dev/tutorial/context-api) of your root component. This means that `page` is unique to each request on the server, rather than shared between multiple requests handled by the same server simultaneously.
+ * Stores on the server are _contextual_ — they are added to the [context](https://svelte.dev/tutorial/context-api) of your root component. This means that `page` is unique to each request, rather than shared between multiple requests handled by the same server simultaneously.
  *
  * Because of that, you must subscribe to the stores during component initialization (which happens automatically if you reference the store value, e.g. as `$page`, in a component) before you can use them.
+ *
+ * In the browser, we don't need to worry about this, and stores can be accessed from anywhere. Code that will only ever run on the browser can refer to (or subscribe to) any of these stores at any time.
  */
 declare module '$app/stores' {
 	import { Readable } from 'svelte/store';
 	import { Navigation, Page } from '@sveltejs/kit';
 
-	/**
-	 * A convenience function around `getContext`. Must be called during component initialization.
-	 * Only use this if you need to defer store subscription until after the component has mounted, for some reason.
-	 */
-	export function getStores(): {
-		navigating: typeof navigating;
-		page: typeof page;
-		updated: typeof updated;
-	};
-
 	/**
 	 * A readable store whose value contains page data.
 	 */
@@ -248,6 +240,16 @@ declare module '$app/stores' {
 	 *  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.
 	 */
 	export const updated: Readable<boolean> & { check: () => boolean };
+
+	/**
+	 * A function that returns all of the contextual stores. On the server, this must be called during component initialization.
+	 * Only use this if you need to defer store subscription until after the component has mounted, for some reason.
+	 */
+	export function getStores(): {
+		navigating: typeof navigating;
+		page: typeof page;
+		updated: typeof updated;
+	};
 }
 
 /**


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte b/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte
new file mode 100644
index 000000000000..d6fb292633e2
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/store/client-access/+page.svelte
@@ -0,0 +1,13 @@
+<script>
+	import { getStores } from '$app/stores';
+
+	let pathname;
+</script>
+
+<h1>{pathname}</h1>
+
+<button
+	on:click={() => {
+		getStores().page.subscribe(($page) => pathname = $page.url.pathname)();
+	}}>click</button
+>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 05be5cf424ec..f95a9568de7c 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -585,3 +585,10 @@ test('Can use browser-only global on client-only page', async ({ page, read_erro
 	await expect(page.locator('p')).toHaveText('Works');
 	expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined);
 });
+
+test('can use $app/stores from anywhere on client', async ({ page }) => {
+	await page.goto('/store/client-access');
+	await expect(page.locator('h1')).toHaveText('undefined');
+	await page.click('button');
+	await expect(page.locator('h1')).toHaveText('/store/client-access');
+});