-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
breaking: adjust how pushState/replaceState behave
#16389
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
Changes from all commits
e612de2
20e2079
b4d7f15
02c1322
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@sveltejs/kit': minor | ||
| --- | ||
|
|
||
| breaking: adjust how `pushState/replaceState` behave |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@sveltejs/kit': minor | ||
| --- | ||
|
|
||
| feat: provide possibility to preserve state across reloads when using `pushState/replaceState` | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,8 +15,8 @@ SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState) | |||||||||
| import { page } from '$app/state'; | ||||||||||
| import Modal from './Modal.svelte'; | ||||||||||
|
|
||||||||||
| function showModal() { | ||||||||||
| pushState('', { | ||||||||||
| async function showModal() { | ||||||||||
| await pushState('', { | ||||||||||
| showModal: true | ||||||||||
| }); | ||||||||||
| } | ||||||||||
|
|
@@ -31,11 +31,25 @@ The modal can be dismissed by navigating back (unsetting `page.state.showModal`) | |||||||||
|
|
||||||||||
| ## API | ||||||||||
|
|
||||||||||
| The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`. | ||||||||||
| The first argument to `pushState` is the URL, relative to the current URL. A non-empty URL performs a shallow navigation: [`beforeNavigate`]($app-navigation#beforeNavigate), [`onNavigate`]($app-navigation#onNavigate), [`onNavigate`]($app-navigation#onNavigate), [`onNavigate`]($app-navigation#onNavigate) and [`afterNavigate`]($app-navigation#afterNavigate) will run with `navigation.type === 'shallow'`. | ||||||||||
|
|
||||||||||
| Once shallow routing is active, `page.shallow` is set with the URL, parameters and route id that is user-visible. `page.url`, `page.params` and `page.route` remain as they were before the shallow navigation. | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| To keep the current URL, use `''`. This only updates history and page state, and does not call navigation lifecycle functions. If the page is already showing a shallow route, `page.shallow` is preserved; otherwise it remains `null`. | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm I'm in two minds about whether it's desirable to skip lifecycle functions for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be really annoyed as a user if that was the case. The 99% use case for |
||||||||||
|
|
||||||||||
| Passing `null` as the first argument will exit shallow routing (as do `goto` or link clicks). This reverts the visible URL to what it was before and sets `page.shallow` to `null`. | ||||||||||
|
|
||||||||||
| 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`). | ||||||||||
|
|
||||||||||
| To set page state without creating a new history entry, use `replaceState` instead of `pushState`. | ||||||||||
| After a shallow navigation, `page.shallow` contains the target `url`, `params` and `route`. The actual page has not changed, so `page.url`, `page.params` and `page.route` continue to describe the page that is currently rendered. | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this feels feels duplicative with the paragraph above (https://github.com/sveltejs/kit/pull/16389/files#r3616836269). I would probably lose that one and keep this one |
||||||||||
|
|
||||||||||
| `pushState` and `replaceState` return promises that resolve after the page state change has been applied to the DOM. To set page state without creating a new history entry, use `replaceState` instead of `pushState`. | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these two things are unconnected, we should probably explain the push/replace distinction earlier |
||||||||||
|
|
||||||||||
| By default, state set through `pushState` or `replaceState` is discarded on a full page reload. Pass `{ persist: true }` as the third argument to restore it in the browser after reloading, independently of whether the call performs a shallow navigation: | ||||||||||
|
|
||||||||||
| ```js | ||||||||||
| await pushState('', { showModal: true }, { persist: true }); | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ## Loading data for a route | ||||||||||
|
|
||||||||||
|
|
@@ -74,7 +88,7 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c | |||||||||
| const result = await preloadData(href); | ||||||||||
|
|
||||||||||
| if (result.type === 'loaded' && result.status === 200) { | ||||||||||
| pushState(href, { selected: result.data }); | ||||||||||
| await pushState(href, { selected: result.data }); | ||||||||||
| } else { | ||||||||||
| // something bad happened! try navigating | ||||||||||
| goto(href); | ||||||||||
|
|
@@ -96,6 +110,6 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c | |||||||||
|
|
||||||||||
| ## Caveats | ||||||||||
|
|
||||||||||
| 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. | ||||||||||
| During server-side rendering, `page.state` is always an empty object. State set through `goto` is always restored in the browser after a reload. State set through `pushState` or `replaceState` is only restored if `{ persist: true }` was passed. | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| 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. | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1268,8 +1268,9 @@ export interface NavigationTarget< | |
| * - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring | ||
| * - `link`: Navigation was triggered by a link click | ||
| * - `popstate`: Navigation was triggered by back/forward navigation | ||
| * - `shallow`: Navigation was triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL | ||
| */ | ||
| export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; | ||
| export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate' | 'shallow'; | ||
|
|
||
| export interface NavigationBase { | ||
| /** | ||
|
|
@@ -1280,6 +1281,7 @@ export interface NavigationBase { | |
| * - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring | ||
| * - `link`: Navigation was triggered by a link click | ||
| * - `popstate`: Navigation was triggered by back/forward navigation | ||
| * - `shallow`: Navigation was triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need some way to identify shallow navigations in popstate events. Otherwise you can't really filter them out —
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in fact I'm almost wondering if we should revisit the decision to keep await goto('', {
state: {...},
shallow: true
});We even have a
Yeah, the more I think about it the more I think this is the right move.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What exactly would I be doing with that info? i.e. what's the use case of boolean vs type?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. boolean: // works for the initial shallow navigation, and a backwards navigation
if (navigation.shallow) return;type: // works for the initial shallow navigation, BUT NOT a backwards navigation
if (navigation.type === 'shallow') return;If there's something I need to happen or not happen during a transition between shallow and non-shallow routing, |
||
| */ | ||
| type: NavigationType; | ||
| /** | ||
|
|
@@ -1327,6 +1329,13 @@ export interface NavigationGoto extends NavigationBase { | |
| type: 'goto'; | ||
| } | ||
|
|
||
| /** | ||
| * A navigation triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL | ||
| */ | ||
| export interface NavigationShallow extends NavigationBase { | ||
| type: 'shallow'; | ||
| } | ||
|
|
||
| /** | ||
| * A navigation triggered by the tab being closed, or the user navigating to a different document | ||
| */ | ||
|
|
@@ -1379,7 +1388,8 @@ export type Navigation = | |
| | NavigationExternal | ||
| | NavigationFormSubmit | ||
| | NavigationPopState | ||
| | NavigationLink; | ||
| | NavigationLink | ||
| | NavigationShallow; | ||
|
|
||
| /** | ||
| * The argument passed to [`beforeNavigate`](https://svelte.dev/docs/kit/$app-navigation#beforeNavigate) callbacks. | ||
|
|
@@ -1453,6 +1463,17 @@ export interface Page< | |
| * The page state, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. | ||
| */ | ||
| state: App.PageState; | ||
| /** | ||
| * Information about the target of the most recent shallow navigation, or `null` if no shallow navigation has occurred. | ||
| */ | ||
| shallow: { | ||
| /** Parameters of the target route, or `null` if the URL does not resolve to a route. */ | ||
| params: AppLayoutParams<'/'> | null; | ||
| /** Info about the target route, or `null` if the URL does not resolve to a route. */ | ||
| route: { id: AppRouteId } | null; | ||
| /** The normalized URL passed to `pushState` or `replaceState`. */ | ||
| url: ReadonlyURL; | ||
| } | null; | ||
| /** | ||
| * Filled only after a form submission. See [form actions](https://svelte.dev/docs/kit/form-actions) for more info. | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we were going to recommend using snapshot instead of this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can have both — someone could (for example) open a modal that they want to recreate on reload, and snapshots aren't really built for that