Skip to content
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

keep browser stores in always-accessible singleton #6100

Merged
merged 5 commits into from Aug 20, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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__');

return {
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