Resetting components on navigation #5007
Replies: 18 comments 14 replies
-
I find it to be enough to initialize the page's state through props in <!-- src/routes/[whatever].svelte -->
<script context="module">
export const load = async (args) => {
return {
props: {
// Initial page state (gets mutated during page lifespan)
open: false,
// ... Other props that you don't mutate
}
};
};
</script>
<script>
// People would otherwise write `let open = false;` and run into the described problem
export let open;
// Mutate state
const toggle = () => {
open = !open;
}
</script>
<button on:click={toggle}>Toggle</button>
{#if open}Expanded text{/if} When components are used in the page, props should be passed (perhaps via binding) to them instead of them having state that the parent can't control. Can anyone who has the described problem share an example so I / we can try to solve it with a variation of this technique? |
Beta Was this translation helpful? Give feedback.
-
Instantiating a new component on navigate is a lot easier to reason about. SvelteKit is becoming quite a complex beast, I'm inclined towards making things easier to reason about over performance. So long as the performance difference is small. I like the idea of instantiation by default, but possible to disable this default behaviour via a function. |
Beta Was this translation helpful? Give feedback.
-
I have a problem with this approach. There can't be any "dumb" pages i.e. with just component logic, no explicit network calls or load. I would like to use page endpoints wherever possible to pass data onto pages. But with this approach, I will need to have an extra script tag with context="module". I would suggest having a special function / store that will receive a callback. This callback will return an object containing any derived properties of the route. Only the value return by this function will be re-evaluated on route params / props change. const state = derive(() => {
return {
stats: getStats()
};
}); |
Beta Was this translation helpful? Give feedback.
-
I'm thinking about the case where you only want to consider certain params, say something major can change, but something minor can also change where you want to preserve:
I think "navigating" will have a value at this point? So you could do this? <script context="module">
import { navigating } from '$app/stores';
export function load() {
let { from, to } = $navigating;
let m1 = from.pathname.match(/^\/catalog\/(\d+)/);
let m2 = to.pathname.match(/^\/catalog\/(\d+)/);
return {
preserve: m1 && m2 && m1[1] === m2[1]
};
</script> |
Beta Was this translation helpful? Give feedback.
-
hmm, can you do
to selectively force recreating components on data-only changes? |
Beta Was this translation helpful? Give feedback.
-
As someone who found the native behavior unintuitive I think this is the most important part, regardless of the proposed solution. I'm not an experienced front-end developer and it wasn't obvious to me that navigating between pages was being handled as an SPA instead of a browser navigation, and I think that's where the confusion tends to come from. In my opinion it would be very useful to have a note about this behavior on the dynamic parameters section of the documentation and a recommended workaround/solution. |
Beta Was this translation helpful? Give feedback.
-
I think flipping the default around to assume If you think about it, whatever we do now for navigating around the tree must be a good experience, so why isn't that good enough also for sideways navigation among the same route? Concerns about performance seem more theoretical than everyday practical. Not sure about layouts though, can we just do whatever we do today when navigating between route A and route B that both share a layout? Just match that behavior? I suppose this would be "preserve layouts, reset leaves". Also I had an idea about my "conditional preserve" use case... can we take inspiration from HTTP etags and offer a second option, something like:
One common case I imagine is preserving when only the query string changes, otherwise reset, which you could do with:
Where |
Beta Was this translation helpful? Give feedback.
-
Wait why do people expect component would be destroyed and re-mounted on navigation ? I have used several client side navigation libraries over the years, and I see the same behavior what Sveltekit is using at the moment, very baffling 🤔 |
Beta Was this translation helpful? Give feedback.
-
Registering my vote for "preserve: false for leaves, preserve: true for layouts" Thanks for treating this issue like it matters. I think it's really important to get right. Just a note on the performance benefit: I've always been a fan philosophically of write first, optimize later. So having preserve:false for leaves is what I would expect to use 90%+ of the time. If I need a nice transition or I'm running into a performance bottleneck, I have the ability to optimize later. |
Beta Was this translation helpful? Give feedback.
-
You probably mean Generally I really like having a "preserve" option. Caused quite some confusion for me as well, when my "locale" prop on my Header changed, but the navbar didn't update (I had the link labels in a map, and had to wrap the creation of the map into a $: to make it reactive and update when the locale changed). Really confused me until I found out this was caused by that, so I can see how having preserve: false as default would be helpful. Another idea: what about having an option to restructure / "fake an update" for only some variables, or only restructure them / the component if only a certain variable (which can be configured) has changed? |
Beta Was this translation helpful? Give feedback.
-
It'll be interesting to revisit this discussion after the dust settles on #5748 |
Beta Was this translation helpful? Give feedback.
-
I personally also support My reasons are as follows: 1] On a typical website (or at least most websites I can think of) pages like This is also sort of a counterargument to these cons:
Pages should be reset because the pages generally depend on the dynamic parameters which changed, whereas layouts generally do not depend on the dynamic parameters and therefore stay the same and should not be reset. Obviously this reasoning does not apply for all websites and you could construct a counterexample, however at least a cursory glance at websites I commonly use makes me reasonably confident that it does apply for a majority of websites. And for those where it does not apply, the option to override the default could be used. 2] The current system makes it more error prone and complicated to deal with data that the user can edit, especially when saving data to the server is involved. I find that it is annoyingly easy to accidentally leak state between two different pages when the component is preserved during navigation. Preserving components adds another layer to the page component's lifecycle which makes reasoning about things more complex and error prone. For example, let's say you have a form that allows the user to edit a photo's details at As I understand it, the current approach is to use For example, let's say you have a variable on your photos page that tracks the last saved version of the data and the user navigates from |
Beta Was this translation helpful? Give feedback.
-
Some more prior art based on what I could figure out about Angular and Next.js:
So overall it seems that Nuxt is the odd one out here in preferring the hybrid approach, however Next does offer the option to remount based on a key if you want to and I suspect Angular has the relevant configuration somewhere, but I don't know where. |
Beta Was this translation helpful? Give feedback.
-
If the main rationale for this feature is that the current behaviour has caused confusion, then surely the lowest hanging fruit is just more explicit documentation? I can only speak for my own experience, but destroying and remounting pages between routes goes against one of the key benefits of an SPA, and feels like a pretty niche use-case. Unless there's proven solid demand for it I feel adding this feature rather than just suggesting to cleanup on |
Beta Was this translation helpful? Give feedback.
-
To be clear, I propose and support the following behavior: /photos/1 and /photos/2 should be considered two distinct pages, even if the underlying component is the same, and navigating between them should remount. This is useful because I do not want these two pages to ever have any shared state. However, navigating from /photos/1 to itself or to /photos/1?u=7#details should not remount, because this would be the same page with the same content and therefore I want state to be preserved. Intuitively, the system should behave as if there are /photos/1.svelte and /photos/2.svelte components, even if there is in fact a single component with a dynamic param. All other behavior could stay the same as it is now. This behavior would match Nuxt 3 and Routify with param-is-page. |
Beta Was this translation helpful? Give feedback.
-
When we're talking about the state it should never be implicit. I ate my brain with a
I think the mental model of most developers who start with Svelte would be like this: path changed -> components are re-rendered, especially if the path is a part of deciding what and how is going to be rendered. All those MDSvex Blog examples follow this pattern at least. |
Beta Was this translation helpful? Give feedback.
-
I think a clean way to solve this is to make the current URL a writable store, then if your component isn't mounting, you have a clean spot to subscribe, with an interface you're probably already using. |
Beta Was this translation helpful? Give feedback.
-
A page like
/photos/1
can be represented like this:This is pseudo-code — in reality it's a bit more complicated — but it's basically what happens if you have a root
__layout.svelte
and aphotos/__layout.svelte
file in addition to yourphotos/[id].svelte
.If the user navigates from
/photos/1
to/photos/2
, then the component tree is unchanged — SvelteKit just updatesprops.post
. This is deliberate, for two reasons:But this has an impact on application design — the internal state of
<Photo>
is preserved. For example if the photo page has a collapsible 'detail' box showing the photo's EXIF data, the collapsed state will persist across navigations. If that's not what you want, you can reset the state in anafterNavigate
function, but you need to make sure that any new non-persisted state added later on is also reset.For some people, this is confusing:
Proposed solution
Personally I'm inclined to argue that the
afterNavigate
solution is totally fine, as long as we do a good enough job of documenting what happens when the user navigates. We don't normally expect components to be destroyed and remounted on state changes, and navigation is arguably just a kind of state change.But since this has been a source of confusion, it's worth considering what a solution might look like. One suggestion was to provide a 'key' that could be used to determine whether a component should be preserved:
This is an interesting idea, but it wouldn't work in the case where you click a link to the page you're currently on (which should result in a reset if the route would normally result in a reset when you navigate between pages that belong to it). You could maybe return
Math.random()
, but... no.I'm therefore proposing an alternative approach — returning a
preserve
key fromload
:On navigation, the node closest to the root with
preserve: false
(whether that's the page component — the 'leaf' — or a layout component) would be briefly removed from the tree......then put back:
Defaults
Assuming people a) think this is an important problem to solve, and b) like the proposed API, we need to talk about defaults. As I see it there are three options:
preserve: true
, the status quoPros:
Cons:
preserve: false
Pros:
Cons:
preserve: false
for leaves,preserve: true
for layoutsPros:
Cons:
Prior art
I looked at other popular frameworks that have nested routes:
Beta Was this translation helpful? Give feedback.
All reactions