Skip to content

Commit f2393eb

Browse files
Rich-Harrisdummdidummteemingc
authored
feat: add $app/state module (#13140)
* WIP * most existing stuff working * working * paperwork * get $app/state working * prettier * stopgap * this seems to work * update most of the test files * fix * oops * navigating * whitespace is important for our test assertion * fix * adjust new tests/folder names, keep around old $app/stores-specific tests until SvelteKit 3 * adjust test * updated * docs * better dev time errors * another test * adjust documentation * nomenclature * lint * adjust test name * oops * changeset * deprecate $app/stores * fix hash link + typo * Update documentation/docs/60-appendix/40-migrating.md Co-authored-by: Tee Ming <[email protected]> * fix/tweak * typos * document deprecation, link between state/stores * Apply suggestions from code review * regenerate * tweak wording * small tweaks --------- Co-authored-by: Simon Holthausen <[email protected]> Co-authored-by: Tee Ming <[email protected]>
1 parent 0464283 commit f2393eb

File tree

122 files changed

+943
-271
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+943
-271
lines changed

.changeset/modern-news-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add `$app/state` module

documentation/docs/10-getting-started/40-web-standards.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Most of the time, your endpoints will return complete data, as in the `userAgent
7878
7979
## URL APIs
8080
81-
URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](hooks) and [server routes](routing#server), [`$page.url`]($app-stores) in [pages](routing#page), `from` and `to` in [`beforeNavigate` and `afterNavigate`]($app-navigation) and so on.
81+
URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](hooks) and [server routes](routing#server), [`page.url`]($app-state) in [pages](routing#page), `from` and `to` in [`beforeNavigate` and `afterNavigate`]($app-navigation) and so on.
8282
8383
### URLSearchParams
8484

documentation/docs/20-core-concepts/10-routing.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,15 @@ If an error occurs during `load`, SvelteKit will render a default error page. Yo
132132
```svelte
133133
<!--- file: src/routes/blog/[slug]/+error.svelte --->
134134
<script>
135-
import { page } from '$app/stores';
135+
import { page } from '$app/state';
136136
</script>
137137
138-
<h1>{$page.status}: {$page.error.message}</h1>
138+
<h1>{page.status}: {page.error.message}</h1>
139139
```
140140

141+
> [!LEGACY]
142+
> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead.
143+
141144
SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and then `src/routes/+error.svelte` before rendering the default error page. If _that_ fails (or if the error was thrown from the `load` function of the root `+layout`, which sits 'above' the root `+error`), SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file.
142145

143146
If the error occurs inside a `load` function in `+layout(.server).js`, the closest error boundary in the tree is an `+error.svelte` file _above_ that layout (not next to it).

documentation/docs/20-core-concepts/20-load.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,14 @@ Data returned from layout `load` functions is available to child `+layout.svelte
116116
```svelte
117117
/// file: src/routes/blog/[slug]/+page.svelte
118118
<script>
119-
+++import { page } from '$app/stores';+++
119+
+++import { page } from '$app/state';+++
120120
121121
/** @type {{ data: import('./$types').PageData }} */
122122
let { data } = $props();
123123
124124
+++ // we can access `data.posts` because it's returned from
125125
// the parent layout `load` function
126-
let index = $derived(data.posts.findIndex(post => post.slug === $page.params.slug));
126+
let index = $derived(data.posts.findIndex(post => post.slug === page.params.slug));
127127
let next = $derived(data.posts[index + 1]);+++
128128
</script>
129129
@@ -137,24 +137,28 @@ Data returned from layout `load` functions is available to child `+layout.svelte
137137

138138
> [!NOTE] If multiple `load` functions return data with the same key, the last one 'wins' — the result of a layout `load` returning `{ a: 1, b: 2 }` and a page `load` returning `{ b: 3, c: 4 }` would be `{ a: 1, b: 3, c: 4 }`.
139139
140-
## $page.data
140+
## page.data
141141

142142
The `+page.svelte` component, and each `+layout.svelte` component above it, has access to its own data plus all the data from its parents.
143143

144-
In some cases, we might need the opposite — a parent layout might need to access page data or data from a child layout. For example, the root layout might want to access a `title` property returned from a `load` function in `+page.js` or `+page.server.js`. This can be done with `$page.data`:
144+
In some cases, we might need the opposite — a parent layout might need to access page data or data from a child layout. For example, the root layout might want to access a `title` property returned from a `load` function in `+page.js` or `+page.server.js`. This can be done with `page.data`:
145145

146146
```svelte
147147
<!--- file: src/routes/+layout.svelte --->
148148
<script>
149-
import { page } from '$app/stores';
149+
import { page } from '$app/state';
150150
</script>
151151
152152
<svelte:head>
153-
<title>{$page.data.title}</title>
153+
<title>{page.data.title}</title>
154154
</svelte:head>
155155
```
156156

157-
Type information for `$page.data` is provided by `App.PageData`.
157+
Type information for `page.data` is provided by `App.PageData`.
158+
159+
> [!LEGACY]
160+
> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead.
161+
> It provides a `page` store with the same interface that you can subscribe to, e.g. `$page.data.title`.
158162
159163
## Universal vs server
160164

documentation/docs/20-core-concepts/30-form-actions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ As well as the `action` attribute, we can use the `formaction` attribute on a bu
102102
103103
## Anatomy of an action
104104
105-
Each action receives a `RequestEvent` object, allowing you to read the data with `request.formData()`. After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available through the `form` property on the corresponding page and through `$page.form` app-wide until the next update.
105+
Each action receives a `RequestEvent` object, allowing you to read the data with `request.formData()`. After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available through the `form` property on the corresponding page and through `page.form` app-wide until the next update.
106106
107107
```js
108108
/// file: src/routes/login/+page.server.js
@@ -156,7 +156,7 @@ export const actions = {
156156
157157
### Validation errors
158158
159-
If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `fail` function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through `$page.status` and the data through `form`:
159+
If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `fail` function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through `page.status` and the data through `form`:
160160
161161
```js
162162
/// file: src/routes/login/+page.server.js
@@ -352,7 +352,7 @@ The easiest way to progressively enhance a form is to add the `use:enhance` acti
352352
353353
Without an argument, `use:enhance` will emulate the browser-native behaviour, just without the full-page reloads. It will:
354354
355-
- update the `form` property, `$page.form` and `$page.status` on a successful or invalid response, but only if the action is on the same page you're submitting from. For example, if your form looks like `<form action="/somewhere/else" ..>`, `form` and `$page` will _not_ be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, use [`applyAction`](#Progressive-enhancement-Customising-use:enhance)
355+
- update the `form` property, `page.form` and `page.status` on a successful or invalid response, but only if the action is on the same page you're submitting from. For example, if your form looks like `<form action="/somewhere/else" ..>`, the `form` prop and the `page.form` state will _not_ be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, use [`applyAction`](#Progressive-enhancement-Customising-use:enhance)
356356
- reset the `<form>` element
357357
- invalidate all data using `invalidateAll` on a successful response
358358
- call `goto` on a redirect response
@@ -411,7 +411,7 @@ If you return a callback, you may need to reproduce part of the default `use:enh
411411
412412
The behaviour of `applyAction(result)` depends on `result.type`:
413413
414-
- `success`, `failure` — sets `$page.status` to `result.status` and updates `form` and `$page.form` to `result.data` (regardless of where you are submitting from, in contrast to `update` from `enhance`)
414+
- `success`, `failure` — sets `page.status` to `result.status` and updates `form` and `page.form` to `result.data` (regardless of where you are submitting from, in contrast to `update` from `enhance`)
415415
- `redirect` — calls `goto(result.location, { invalidateAll: true })`
416416
- `error` — renders the nearest `+error` boundary with `result.error`
417417

documentation/docs/20-core-concepts/50-state-management.md

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Instead, you should _authenticate_ the user using [`cookies`](load#Cookies) and
4040

4141
## No side-effects in load
4242

43-
For the same reason, your `load` functions should be _pure_ — no side-effects (except maybe the occasional `console.log(...)`). For example, you might be tempted to write to a store inside a `load` function so that you can use the store value in your components:
43+
For the same reason, your `load` functions should be _pure_ — no side-effects (except maybe the occasional `console.log(...)`). For example, you might be tempted to write to a store or global state inside a `load` function so that you can use the value in your components:
4444

4545
```js
4646
/// file: +page.js
@@ -76,31 +76,25 @@ export async function load({ fetch }) {
7676
}
7777
```
7878

79-
...and pass it around to the components that need it, or use [`$page.data`](load#$page.data).
79+
...and pass it around to the components that need it, or use [`page.data`](load#page.data).
8080

8181
If you're not using SSR, then there's no risk of accidentally exposing one user's data to another. But you should still avoid side-effects in your `load` functions — your application will be much easier to reason about without them.
8282

83-
## Using stores with context
83+
## Using state and stores with context
8484

85-
You might wonder how we're able to use `$page.data` and other [app stores]($app-stores) if we can't use our own stores. The answer is that app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the store is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own stores:
85+
You might wonder how we're able to use `page.data` and other [app state]($app-state) (or [app stores]($app-stores)) if we can't use global state. The answer is that app state and app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the state (or store) is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own state:
8686

8787
```svelte
8888
<!--- file: src/routes/+layout.svelte --->
8989
<script>
9090
import { setContext } from 'svelte';
91-
import { writable } from 'svelte/store';
9291
9392
/** @type {{ data: import('./$types').LayoutData }} */
9493
let { data } = $props();
9594
96-
// Create a store and update it when necessary...
97-
const user = writable(data.user);
98-
$effect.pre(() => {
99-
user.set(data.user);
100-
});
101-
102-
// ...and add it to the context for child components to access
103-
setContext('user', user);
95+
// Pass a function referencing our state
96+
// to the context for child components to access
97+
setContext('user', () => data.user);
10498
</script>
10599
```
106100

@@ -113,10 +107,15 @@ You might wonder how we're able to use `$page.data` and other [app stores]($app-
113107
const user = getContext('user');
114108
</script>
115109
116-
<p>Welcome {$user.name}</p>
110+
<p>Welcome {user().name}</p>
117111
```
118112

119-
Updating the value of a context-based store in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component because it has already been rendered by the time the store value is updated. In contrast, on the client (when CSR is enabled, which is the default) the value will be propagated and components, pages, and layouts higher in the hierarchy will react to the new value. Therefore, to avoid values 'flashing' during state updates during hydration, it is generally recommended to pass state down into components rather than up.
113+
> [!NOTE] We're passing a function into `setContext` to keep reactivity across boundaries. Read more about it [here](https://svelte.dev/docs/svelte/$state#Passing-state-into-functions)
114+
115+
> [!LEGACY]
116+
> You also use stores from `svelte/store` for this, but when using Svelte 5 it is recommended to make use of universal reactivity instead.
117+
118+
Updating the value of context-based state in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component because it has already been rendered by the time the state value is updated. In contrast, on the client (when CSR is enabled, which is the default) the value will be propagated and components, pages, and layouts higher in the hierarchy will react to the new value. Therefore, to avoid values 'flashing' during state updates during hydration, it is generally recommended to pass state down into components rather than up.
120119

121120
If you're not using SSR (and can guarantee that you won't need to use SSR in future) then you can safely keep state in a shared module, without using the context API.
122121

@@ -163,14 +162,18 @@ Instead, we need to make the value [_reactive_](/tutorial/svelte/state):
163162
Reusing components like this means that things like sidebar scroll state are preserved, and you can easily animate between changing values. In the case that you do need to completely destroy and remount a component on navigation, you can use this pattern:
164163

165164
```svelte
166-
{#key $page.url.pathname}
165+
<script>
166+
import { page } from '$app/state';
167+
</script>
168+
169+
{#key page.url.pathname}
167170
<BlogPost title={data.title} content={data.title} />
168171
{/key}
169172
```
170173

171174
## Storing state in the URL
172175

173-
If you have state that should survive a reload and/or affect SSR, such as filters or sorting rules on a table, URL search parameters (like `?sort=price&order=ascending`) are a good place to put them. You can put them in `<a href="...">` or `<form action="...">` attributes, or set them programmatically via `goto('?key=value')`. They can be accessed inside `load` functions via the `url` parameter, and inside components via `$page.url.searchParams`.
176+
If you have state that should survive a reload and/or affect SSR, such as filters or sorting rules on a table, URL search parameters (like `?sort=price&order=ascending`) are a good place to put them. You can put them in `<a href="...">` or `<form action="...">` attributes, or set them programmatically via `goto('?key=value')`. They can be accessed inside `load` functions via the `url` parameter, and inside components via `page.url.searchParams`.
174177

175178
## Storing ephemeral state in snapshots
176179

documentation/docs/30-advanced/25-errors.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ export async function load({ params }) {
4040
}
4141
```
4242

43-
This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `$page.error` is the object provided as the second argument to `error(...)`.
43+
This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `page.error` is the object provided as the second argument to `error(...)`.
4444

4545
```svelte
4646
<!--- file: src/routes/+error.svelte --->
4747
<script>
48-
import { page } from '$app/stores';
48+
import { page } from '$app/state';
4949
</script>
5050
51-
<h1>{$page.error.message}</h1>
51+
<h1>{page.error.message}</h1>
5252
```
5353

54+
> [!LEGACY]
55+
> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead.
56+
5457
You can add extra properties to the error object if needed...
5558

5659
```js

documentation/docs/30-advanced/67-shallow-routing.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState)
1212
<!--- file: +page.svelte --->
1313
<script>
1414
import { pushState } from '$app/navigation';
15-
import { page } from '$app/stores';
15+
import { page } from '$app/state';
1616
import Modal from './Modal.svelte';
1717
1818
function showModal() {
@@ -22,21 +22,24 @@ SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState)
2222
}
2323
</script>
2424
25-
{#if $page.state.showModal}
25+
{#if page.state.showModal}
2626
<Modal close={() => history.back()} />
2727
{/if}
2828
```
2929

30-
The modal can be dismissed by navigating back (unsetting `$page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically.
30+
The modal can be dismissed by navigating back (unsetting `page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically.
3131

3232
## API
3333

3434
The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`.
3535

36-
The second argument is the new page state, which can be accessed via the [page store]($app-stores#page) as `$page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`).
36+
The second argument is the new page state, which can be accessed via the [page object]($app-state#page) as `page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`).
3737

3838
To set page state without creating a new history entry, use `replaceState` instead of `pushState`.
3939

40+
> [!LEGACY]
41+
> `page.state` from `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$page.state` from `$app/stores` instead.
42+
4043
## Loading data for a route
4144

4245
When shallow routing, you may want to render another `+page.svelte` inside the current page. For example, clicking on a photo thumbnail could pop up the detail view without navigating to the photo page.
@@ -47,7 +50,7 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c
4750
<!--- file: src/routes/photos/+page.svelte --->
4851
<script>
4952
import { preloadData, pushState, goto } from '$app/navigation';
50-
import { page } from '$app/stores';
53+
import { page } from '$app/state';
5154
import Modal from './Modal.svelte';
5255
import PhotoPage from './[id]/+page.svelte';
5356
@@ -85,17 +88,17 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c
8588
</a>
8689
{/each}
8790
88-
{#if $page.state.selected}
91+
{#if page.state.selected}
8992
<Modal onclose={() => history.back()}>
9093
<!-- pass page data to the +page.svelte component,
9194
just like SvelteKit would on navigation -->
92-
<PhotoPage data={$page.state.selected} />
95+
<PhotoPage data={page.state.selected} />
9396
</Modal>
9497
{/if}
9598
```
9699

97100
## Caveats
98101

99-
During server-side rendering, `$page.state` is always an empty object. The same is true for the first page the user lands on — if the user reloads the page (or returns from another document), state will _not_ be applied until they navigate.
102+
During server-side rendering, `page.state` is always an empty object. The same is true for the first page the user lands on — if the user reloads the page (or returns from another document), state will _not_ be applied until they navigate.
100103

101104
Shallow routing is a feature that requires JavaScript to work. Be mindful when using it and try to think of sensible fallback behavior in case JavaScript isn't available.

0 commit comments

Comments
 (0)