Description
Describe the problem
There are a number of cases where it's valuable to be able to read and write state associated with a particular history entry. For example, since there's no concept of bfcache when doing client-side navigation, things like scroll position (for elements other than the body) and form element values are discarded.
Associating user-controlled state with history entries would also make it possible to do things like history-controlled modals, as described in #3236 (comment).
Describe the proposed solution
When creating a new history entry with history.pushState
or history.replaceState
(in user-oriented terms, navigating via a <a>
click intercept or goto(...)
), we create a new empty state object and store it in a side table (using a similar mechanism to scroll_positions
, which is serialized to sessionStorage
when the user navigates cross-document).
Reading state
After navigation, this object is available in afterNavigate
...
import { afterNavigate } from '$app/navigation';
afterNavigate(({ from, to, state }) => {
console.log(state); // {} — though perhaps it could also be `null` for new entries? not sure
});
...and in a store:
import { page } from '$app/stores';
$: console.log($page.state); // {}
Writing state
So far, so useless. But we can write to the current state
right before we leave the current history entry using beforeNavigate
:
import { beforeNavigate, afterNavigate } from '$app/navigation';
/** @type {HTMLElement} */
let sidebar;
beforeNavigate(({ from, to, state }) => {
state.sidebar_scroll = sidebar.scrollY;
});
Then, if we go back to that entry, we can recover the state:
afterNavigate(({ from, to, state }) => {
sidebar.scrollTo(0, state.sidebar_scroll ?? 0);
});
Programmatically setting state
You might want to show a modal that can be dismissed with the back button (or a backwards swipe, or whatever device/OS-specific interaction signals 'back'). Together with shallow navigation (the concept, if not the name), you could do that with goto
:
goto(null, {
state: {
modal: true
},
shallow: true // or `navigate: false` — see https://github.com/sveltejs/kit/issues/2673#issuecomment-1091736795
});
null
means 'the current URL' and is open to bikeshedding
Then, a component could have something like this:
{#if $page.state.modal}
<Modal on:close={() => history.back()}>
<!-- modal contents -->
</Modal>
{/if}
Closing the modal would cause a backwards navigation; a backwards navigation (triggered via the modal's 'close' button or the browser's chrome) would close the modal.
Alternatives considered
The main alternative is to attempt to automatically track the kind of state that people would want to store (i.e. scroll positions, form element values) so as to simulate the behaviour of a cross-document navigation with bfcache enabled. This comes with some real implementation challenges (capturing the data isn't trivial to do without risking performance issues, and there's no way to reliably determine equivalence between two separate DOM elements), but moreover I'm not certain that it's desirable. Things like automatically populating form elements can definitely go wrong.
One aspect of the design that I'm not sure about is whether the state should be a mutable object or an immutable one. Probably immutable (especially since the Navigation API, which we'd like to adopt eventually, uses immutable state), which makes me wonder if we need to expose methods for getting/setting state inside beforeNavigate
and afterNavigate
rather than just a state
object.
We might also need some way to enforce that state is serializable (most likely as JSON) so that it can be persisted to sessionStorage
, so that it can be recovered when traversing back from another document. Then again perhaps documentation is the solution?
Importance
would make my life easier
Additional Information
No response