Skip to content

Commit

Permalink
keep browser stores in always-accessible singleton (#6100)
Browse files Browse the repository at this point in the history
* keep browser stores in always-accessible singleton

* update docs

* add test

* add changeset

* update getStores docs
  • Loading branch information
Conduitry committed Aug 20, 2022
1 parent 0842fb3 commit b9d709a
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-years-leave.md
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Allow `$app/stores` to be used from anywhere on the browser
5 changes: 4 additions & 1 deletion packages/kit/src/core/sync/write_root.js
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion 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;
Expand All @@ -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__');

const readonly_stores = {
page: {
Expand Down
18 changes: 2 additions & 16 deletions packages/kit/src/runtime/client/client.js
@@ -1,22 +1,15 @@
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';

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';
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions 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;

Expand All @@ -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()
};
@@ -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
>
7 changes: 7 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Expand Up @@ -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');
});
24 changes: 13 additions & 11 deletions packages/kit/types/ambient.d.ts
Expand Up @@ -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.
*/
Expand All @@ -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;
};
}

/**
Expand Down

0 comments on commit b9d709a

Please sign in to comment.