Skip to content

Provide access to history state #5478

Open
@Rich-Harris

Description

@Rich-Harris

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature / enhancementNew feature or requestp1-importantSvelteKit cannot be used by a large number of people, basic functionality is missing, etc.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions