-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Shallow routing with pushState
and replaceState
#9847
Conversation
|
👀 |
It's really hard to make a webapp that feels both app-like and web-like without this. I have a lot of views that I want to just open in a temporary overlay when you navigate to it through the ui, because this feels very app-like. But I want the url to reflect the thing itself, so that if you share the url, then thing itself opens directly to its own page, because this feels very web-like. |
Please excuse ping @Rich-Harris 😄 I was wondering if there is an ETA for this feature to see if its worth waiting or making a custom implementation ourselves. Thanks! |
After reading through this thread and it's predecessor 2673 and seeing the careful forethought that has gone into this feature, I think it's important to note the importance of the query parameters feature, possibly even over shallow path changing - especially as relates to a data organization and user experience perspective. Similar to database design, it's usually better to have one index point to one item, and also a URL representing one singular and primary page of a website. Once a URL can point to two totally different pages of a website (or a user can be on two different pages and see the same URL), user experience can suffer - if you've ever shared the wrong link intending to send the main content page, but instead sent the modal content page, you'll know what I'm getting at. I'd say for most if not all of the use cases described in these threads it's the same, and argue instagram has a flawed design. Is there any case where it wouldn't be better for instagram to use a query param for the modal content? Then when someone send the link, the recipient will see exactly what the sender saw - the main page behind with the modal content intended to be shared above - good UX 👍 There's also many shopping sites that allow you to do a modal quick product view, and then if you wish, click further in on that product to it's canonical page, for which the URL should definitely change, meaning the modal view should be using query params probably. Add to this that query params are what's going to be used most of the time for appending search and filter data to a URL much like Zillow or other real estate sites for example, or google maps when the user zooms/pans the map and google does a shallow routing, or most online shopping sites where users do search and filter. Sorry for long post! |
@joehenry087 I don't really agree. While Instagram may have abused web standards, the resulting design is great for UX.
The goal is to point to the same thing in different contexts. Any other use would be abusing this feature, but that is the responsibility of the developer.
There are some side effects of this approach:
Many teams have gone to great lengths to implement this feature because they know it is the best approach for the problem. Personally, I would have implemented this method in multiple places if it were feasible and easy to maintain. That's why I'm excited for SvelteKit to take care of the heavy lifting. |
** edit, I had a response to your points @Antoine-lb but I realized it's all off topic. My primary point, regardless of the use of shallow routing, is that query params are still a massively important part of web dev, more heavily relied upon than shallow routing I'd guess, which is relatively new, and to simply advocate to prioritize them over shallow routing. If there's more community support to prioritize shallow routing, then so be it. |
Please release this feature mr. Rich🙏🙏. It's been 5 months, I can't wait any longer. |
@joehenry087 I completely agree that query parameters should take priority over shallow routing. It's just more "web". However, there's nothing stopping you from implementing this now - SvelteKit is well-equipped to handle it. Shallow routing, on the other hand, can get messy if you try to do it on your own. That's why we're excited about it |
@Antoine-lb do you have an example on how to implement the query parameters feature handy? In my initial testing I was finding it very difficult to do because pushing to history API would interfere with the Svelte |
@joehenry087 I'm happy to discuss further but we are getting off-topic, you can email me: |
This would be a great addition to kit. It would be awesome if it was possible to implement this behavior with a <script>
import { page } from "$app/stores";
import { pushState } from "$app/navigation";
$: modal_opened = $page.state.modal_opened;
function openModal(e) {
const { href } = e.currentTarget;
pushState({ modal_opened: !modal_opened }, href);
}
</script>
<a href="." on:click|preventDefault={openModal}>
<Modal opened={modal_opened} /> We could write: <a href="." data-sveltekit-shallow-pushstate={{ modal_opened: !$page.state.modal_opened }}>
<Modal opened={$page.state.modal_opened} /> |
Is there anything serious blocking this? Or just the todos? This would be an absolutely incredible feature. Can't wait. |
Closing in favour of #11307 |
This implements the
pushState
/replaceState
idea from #2673.It adds a new
$page.state
object which can be set by callingpushState(state, url?)
orreplaceState(state, url?)
(if theurl
is omitted, it defaults to the current URL). This object is linked to the created/replaced history entry, meaning that if you hit the back button, the state reverts to what it was before. On the server,$page.state
is always{}
.This is not the same as
goto
.goto
causes a navigation, meaning we import the components for the next page, load its data, update$page.url
,$page.params
and$page.route
, and reset scroll/focus — none of which we want in thepushState
/replaceState
case.The key insight here is that we need to track the history stack and the navigation stack separately. Previously they were conflated, which was mostly harmless (though we need special kludgy handling for
popstate
events that only changed the URL hash), but that doesn't suffice if some history entries are associated with navigations and some aren't. In this PR, a typical navigation will increment the history index and the navigation index together, but apushState
will only increment the history index.An alternative API would be
goto(url, { navigate: false })
(orskipLoad: true
in #2637, though as mentionedload
is only one part of navigation), but I think it's conceptually quite different, and this PR shows that the underlying implementation is too. For that reason I prefer a purpose-built API.Another alternative proposal was to add a
data-sveltekit-intercept
attribute which caused the link in question to open in a named slot. I'm not sure this would be possible to implement given how slots currently work in Svelte, but in any case I think the programmatic approach will ultimately prove more flexible — if you wanted to implement something like this Y2K article, where each link takes you successively deeper to an unknown depth, it would be relatively easy to implement programmatically but (I suspect) quite a bit harder declaratively:I'm trying this out in an app I'm building, and it works rather nicely. Here's some code for showing a modal:
I tried implementing this without
pushState
and it was basically impossible, because as soon as you start adding your own history entries, SvelteKit doesn't understand when it needs to navigate.preloadData
I'm also using
pushState
to implement Instagram-style navigation, where clicking on an item shows the detailed view inside a modal instead of navigating to the page. In this case, I want to load the detailed data (including comments etc), but I don't want to reimplement the data loading code (or move stuff into an API route, etc).At the same time, I want the existing preload logic to work — if I hover over a link and SvelteKit starts eagerly fetching the data (intelligently, i.e. not fetching still-valid layout data) in anticipation of a navigation, I want to be able to reuse the stuff it preloaded.
It turns out we can solve both problems by adding a return value to
preloadData
. Here's some code for intercepting a click on a photo and showing the detailed view in a modal:On the same page:
Here,
<PhotoPage>
is an imported+page.svelte
page. I'm being lazy by using a static import, but it could equally be imported dynamically so that we can use code-splitting if we so chose.Because
result.data
is theApp.PageData
object that would be passed to+page.svelte
normally,data={$page.state.selected}
means the page receives identical data to what it normally would. That said, if we wanted to expose anembedded
prop or similar in order to change something on the page, we easily could — it's all within the app developer's control.The page in question does need to be aware of its surroundings —
$page.route
will differ between the modal and full-page cases, and<form use:enhance>
will effectively reload the page becauseinvalidateAll()
will load data forlocation.href
rather than$page.url
(though maybe we should change that?) — you need to provide a customuse:enhance
callback to get the best results. I think this is an acceptable trade-off.updateQuery
Something we talked about in #2673 that isn't implemented here is a separate
updateQuery
function. The desire is to update$page.url.searchParams
without causing a navigation. I haven't thought that all the way through so have omitted it for now.goto(url, { state })
The existing
goto
function takes astate
option, which is somewhat vestigial. It is added tohistory.state
, but messily — SvelteKit's internal history index is rudely shoved onto the object. For the reasons articulated above, there's nothing useful you can actually do with this state, as far as I'm aware.So there's a couple of options:
goto(url, { state })
as deprecated and discourage its use. Don't use it to set$page.state
$page.state
, in which case we can't deprecate it.The question is whether there are cases where you want to be able to set
$page.state
at the same time as causing a programmatic navigation. I'm not sure. If we did deprecate it (which sounds appealing) then you could always do this:handling reloads
Right now there's some bugginess around reloads. If you open a photo in a modal and refresh the page, you go to the full page (as intended), and hitting the back button will take you to the list page you came from, but only because there's special handling for that case. If you open a photo modal and click from there to a new page, then reload that page, clicking back twice won't work as expected. Still looking into how to fix this.
TODO
App.PageState
ambient.d.ts
updateQuery
?goto(url, { state })
should be deprecated or nothistory.pushState
andhistory.replaceState
to discourage their useinvalidateAll()
should probably use$page.url.href
rather thanlocation.href
, since those two things can now be differentPlease don't delete this checklist! Before submitting the PR, please make sure you do the following:
Tests
pnpm test
and lint the project withpnpm lint
andpnpm check
Changesets
pnpm changeset
and following the prompts. Changesets that add features should beminor
and those that fix bugs should bepatch
. Please prefix changeset messages withfeat:
,fix:
, orchore:
.