Open Rich-Harris opened 2 years ago
One thought I just had: the user might scroll or interact with form elements while the navigation is in progress. I wonder if we need a way to write to state immediately before the navigation is committed
Just to connect the two: This would also help with #5478 by giving people easier access to scroll positions.
Just start working on this.
One thing that come to my mind is that if the user changes state
in afterNavigate
we have to notify the $page
store.
But how can we know if the user updated the state.. It's an interior mutability of the same object.
Possible solutions:
getState
, setState
.Proxy
that passthrough getters and setters instead of the real object.Getters and setters sounds better I think
Any ideas on this?
getState and setState make it easier for us but I'm not sure if I like it Design-wise, doesn't feel like something we do in other places. Either way we need to make sure that it's only possible to change state inside the hook.
getState and setState make it easier for us but I'm not sure if I like it Design-wise, doesn't feel like something we do in other places. Either way we need to make sure that it's only possible to change state inside the hook.
Why only in the hook?
Why not $page.state.some_state = x
?
We can only provide the state in the page store and not the actual object in hooks like:
beforeNavigate(() => $page.state = x)
That may be easier for us, and the in client we do:
page.subscribe($page => page_states[CURRENT_INDEX] = $page.state)
This sounds appealing at first, but what if the user presses the back button, what should happen to all the state that was updated outside of the navigation hooks? By keeping it scoped to them, we don't have to answer that question
This sounds appealing at first, but what if the user presses the back button, what should happen to all the state that was updated outside of the navigation hooks? By keeping it scoped to them, we don't have to answer that question
The state that users may change is kept in a page_states
record similar to scroll_positions
that is indexed by history index.
When user clicks back button happens the following:
beforeNavigate(() => $page.state === currentState)
afterNavigate(() => $page.state === previousState)
We update the page store between beforeNavigate
and afterNavigate
like in any forward navigation, the state is not kept in the history.state
but in a plain record that gets saved to sessionStorage
in beforeunload
What if people do $page.state.substate = 'foo'
? That means the state was mutated, it's not possible to get that back. Although now that I've written this down, if we require the state to be JSON-serializable, we can easily clone it.
Still, is it obvious to the user that all state will be lost on navigation / state will be reset to the prior state on back navigation?
What if people do
$page.state.substate = 'foo'
? That means the state was mutated, it's not possible to get that back. Although now that I've written this down, if we require the state to be JSON-serializable, we can easily clone it. Still, is it obvious to the user that all state will be lost on navigation / state will be reset to the prior state on back navigation?
If someone mutates the state like $page.state.key = value
they will only mutate the state related with the current history entry other entries will be untouched
EG: page_states: Record<number, App.PageState>
where number
is the current history index like in scroll_positions
and App.PageState
is an interface that can be typed by the user like in App.Session
We definitely only want to set state at the instant we're navigating to another page (the exception being cross-document navigations, where we need to do it right before unload). Nothing else would really make any sense.
Per https://github.com/sveltejs/kit/issues/5478#issuecomment-1189562537, we can't actually use beforeNavigate
for this. I actually think it might make sense to have a dedicated lifecycle function — maybe onSnapshot
?
onSnapshot(() => {
return {
sidebar_scroll: sidebar.scrollY
};
});
The snapshot would be available in afterNavigate
:
afterNavigate(({ from, to, snapshot }) => {
sidebar.scrollY = snapshot?.sidebar_scroll;
});
I like the name snapshot
for this, I think it does a good job of communicating what it is. I'm less sure about onSnapshot
— don't hate it, but am open to alternatives.
Thinking more about the modal navigation case, I think it's probably a separate concept than snapshot
. state
describes the whole history entry, while snapshot
is a snapshot of page state upon leaving an entry.
state
is something that probably belongs on the $page
store:
<script>
import { page } from '$app/stores';
import { goto } from '$app/navigation';
</script>
<button
on:click={() => {
goto($page.url.href, {
state: {
modal: true
}
})
}
>
show modal
</button>
{#if $page.state.modal}
<div class="modal">...</div>
{/if}
modal navigation
@Rich-Harris Does it totally fall off, will it not be considered? https://github.com/sveltejs/kit/discussions/4560
Seems very hard to understand, honestly. A lot more complex — semantics get very fuzzy around layouts and load
functions etc — and less flexible than an arbitrary state
object
@Rich-Harris My goto(x, {in: y})
means "go to page x while staying on page y".
And I find it fabulously easy to understand.
backpage
/backpages
stores y page (lowest current page).
layout and load functions etc - is fuzzy(truth), but only in the way that it uses slot
, just as layout
uses slot
.
Then y is as if an additional extra layer of layout, and is instead of the normal layout of x. (This is a place for people with more knowledge, whether it should be like that).
Url changes to the one from page x, but the content is from y and from x.
As I write such things, I assume that you, as authors and members, know SvelteKit incomparably better than I do, you know it inside out. I assume that I may not describe something 100%, because I will not predict something. I assume that you can be inspired by what I write, for example, and at least partially use or take and improve it.
I'm afraid (this is my personal feeling and experience) that due to the fact that I am an outsider, there is a certain reluctance to what I will write, even though the content can be, for example, realized one to one after a year and a half (e.g. this https://github.com/sveltejs/svelte/issues/5797#issuecomment-1162544060), and even the justification is as there. Or there is another reason for my unhappiness. (certainly using a translator doesn't help with communication)
As for flexibility - maybe. I was only thinking about modal navigation. But I even considered more complexity, such as using named slots. (I didn't describe it because I thought you just didn't want something flexible, but something more specific. so much flexibility may not even be needed now) Of course, using a simple component gives unlimited flexibility, but it is also less convenient, less easy, you have to implement it more manually. It does not tie the url to a specific page, it can be a blank page. These are disadvantages for me.
I don't know what are the scenarios where goto in
would not solve the problem, or would do it in an ugly way?
The $page.state
itself seems to interfere with that? It could be one with the other.
Realised the snapshot
idea needs more work. Since the snapshot
is only used in afterNavigate
, it wouldn't make sense to have onSnapshot
in a component that didn't after afterNavigate
, so a signature like this might make more sense...
onNavigate(
({ snapshot }) => {
sidebar.scrollTo(0, snapshot?.sidebar_scroll ?? 0);
},
() => ({
sidebar_scroll: sidebar.scrollY
})
);
...except that it's not clear how to associate a particular snapshot with a particular component in a way that survives the component's destruction. If there was only one snapshot for a given history entry that wouldn't be a problem, but you might have a situation like this:
// src/routes/foo/+layout.svelte
onNavigate(
({ snapshot }) => {
console.log(snapshot?.layout_state); // 1
console.log(snapshot?.page_state); // undefined
},
() => ({
layout_state: 1
})
);
// src/routes/foo/+page.svelte
onNavigate(
({ snapshot }) => {
console.log(snapshot?.layout_state); // undefined
console.log(snapshot?.page_state); // 2
},
() => ({
page_state: 2
})
);
Couple of different thoughts:
To clarify, this doesn´t work in ssr right?
Correct, $page.state
would live in sessionStorage
so you wouldn't have access to it during SSR.
use private Svelte APIs
I think we have to treat this as a last resort — the less we have that sort of coupling the better.
add a way to tell that the component, when unmounted, isn't destroyed, rather its state is preserved/the component is detached
This wouldn't solve the problem of navigating offsite then hitting 'back'. It's also a guaranteed source of memory leaks, unfortunately.
It occurs to me that we could reliably associate snapshots with components if we restricted it to page/layout components. Essentially we do this:
<!-- src/routes/some/+page.svelte -->
<script>
export const snapshot = {
capture: () => sidebar.scrollTop,
apply: (y) => sidebar.scrollTo(0, y)
};
</script>
It would be an unusual API but I can't think of any compelling alternatives. The nice thing about this is it's completely orthogonal to beforeNavigate
and afterNavigate
, which means a) we can implement it completely separately from $page.state
, b) it doesn't affect those lifecycle functions, meaning we can remove the breaking change
label, and c) unlike afterNavigate
we only need to run the apply
function if a snapshot exists, meaning developers don't have to handle the no-snapshot case.
So it´s not suitable for modals in my case as i want to support them in ssr too. Any idea/plan how to implement that? I´m currently using searchParams, was just wondering if maybe there is a better (more sveltekit way) solution.
So it´s not suitable for modals in my case as i want to support them in ssr too. Any idea/plan how to implement that? I´m currently using searchParams, was just wondering if maybe there is a better (more sveltekit way) solution.
Hi @david-plugge, did you find a solution? I'm looking to have a modal based route, and don't really have any idea where to start and ideally have SSR. Although guess could give up SSR if this issue makes it simple but don't know the timeline on this issue's priority.
When modals is implemented, could they theoretically be implemented in the same way pages are? It would be really useful if modals could have a load function, action or snapshot. For us at least we usually use modals for forms. Having all those features baked in would be really nice.
@ZetiMente completely missed your comment, very sorry. In case you are still looking for a way to render modals on the server:
<script lang="ts">
import { page } from '$app/stores';
import Modal from './Modal.svelte';
$: showModal = $page.url.searchParams.has('modal');
</script>
{#if showModal}
<Modal />
{/if}
<a href="/?modal">open modal</a>
But keep in mind you cannot properly use sveltekit actions at the moment when the path does not match the path of the action.
I'm not sure whether i'm missing something, but I don't understand why this issue is still open since svelte-kit is not "overwriting" the whole history.state anymore.
I'm able to accomplish everything I need with the following:
// persist component state
history.replaceState(
{
...history.state,
state: {
...history.state.snapshot,
search: state
}
},
'',
window.location.href
);
// restore component state
const state = browser
? history.state?.state?.search ?? {}
: {}
I personally feel this way of interacting with the history seems more flexible than snapshot API, I've tried to accomplish the same with it, but I ended up to the conclusion that is actually harder because snapshots are restored after component initialization while I can just read history.state
on component init that is much better.
With shallow routing implemented, 90% of the things described in this feature request are available now:
$page.state
pushState
/ replaceState
)The thing that's not there yet is manipulating state in beforeNavigate
@dummdidumm "The thing that's not there yet is manipulating state in beforeNavigate"
I just started using pushState/replaseState and realized that they don't trigger beforeNavigate, and so I cannot ask the user to confirm before navigating away from a modal form. I have a rather big webapp that will definitely have complex forms in modals. Users definitely want to be asked for confirmation before loosing complex form state.
Could you open a different issue for that?
Done: #11776
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 https://github.com/sveltejs/kit/issues/3236#issuecomment-1011436921.
Describe the proposed solution
When creating a new history entry with
history.pushState
orhistory.replaceState
(in user-oriented terms, navigating via a<a>
click intercept orgoto(...)
), we create a new empty state object and store it in a side table (using a similar mechanism toscroll_positions
, which is serialized tosessionStorage
when the user navigates cross-document).Reading state
After navigation, this object is available in
afterNavigate
......and in a store:
Writing state
So far, so useless. But we can write to the current
state
right before we leave the current history entry usingbeforeNavigate
:Then, if we go back to that entry, we can recover the state:
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
:Then, a component could have something like this:
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
andafterNavigate
rather than just astate
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