Svelte 5 support
Opened this issue ยท 20 comments
Svelte 5 is right around the corner and while stores are still supported i want to upgrade this library to take advantage of runes.
This however require some changes that are not back compatible so my plan is to continue supporting stores in sveltekit-search-params@^2
and upgrade to runes in sveltekit-search-params@^3
.
The new version however will likely face some api changes because of how runes behave.
queryParameters
The function queryParameters
will benefit a lot from runes: today this function return an unique store object with each query parameter as the key.
<script>
import { ssp, queryParameters } from "sveltekit-search-params";
const params = queryParameters({
search: ssp.string(),
});
</script>
<input bind:value={$params.search} />
with runes the syntax will not change that much but the updates should be much more fine grained which is good
<script>
import { ssp, queryParameters } from "sveltekit-search-params";
const params = queryParameters({
search: ssp.string(),
});
</script>
<input bind:value={params.search} />
queryParam
Here's where the change will be much more intensive so first thing first sorry if you are using this intensively...i'll try to see if i can create a migration CLI to help you with your upgrade.
Today queryParam
accept the key of the query parameter as input and returns a store with that param by itself. If the query parameter is a string the store will contain the string itself.
<script>
import { ssp, queryParam } from "sveltekit-search-params";
const search = queryParam("search");
</script>
<input bind:value={$search} />
This is not possible with runes so we have three options:
The boxed value
The simplest option would to return a ref
or a boxed
instead of the actual value. The downside is that you need to access it with .value
everywhere.
<script>
import { ssp, queryParam } from "sveltekit-search-params";
const search = queryParam("search");
</script>
<input bind:value={search.value} />
This is simple to refactor but once you need to access it with .value
everywhere is it really worth it over using queryParameters
with a short name and just access it like qp.search
everywhere?
The function
Another option could be have the returned value be a function...you could access it by calling it and set it by calling it with a typesafe parameter.
<script>
import { ssp, queryParam } from "sveltekit-search-params";
const search = queryParam("search");
</script>
<input value={search()} oninput={(e)=>{search(e.target.value)}} />
this is nice to see but it can get complex with Typescript type narrowing and it get's pretty hairy with objects and arrays.
The derived
A somewhat nice option would be to make use of $derived.by
to destructure the return value of queryParam
. This would give us a simple value to use everywhere and we could even return an input
object to spread into your inputs that automatically set the oninput
property to mimic the bind
behaviour.
<script>
import { ssp, queryParam } from "sveltekit-search-params";
const [search, inputsearch, setsearch] = $derived.by(queryParam("search"));
</script>
<input {...inputsearch} />
The search is {search}
but this again become very hairy with object and arrays (especially if you want to bind a property of that object instead of the whole object).
Conclusion
I'm not really satisfied with any of the options for queryParam
and i wonder if we should just drop that api but i would love to have YOUR opinion and ideas on this.
Here is a bit of a crazy take, why don't you remove queryParam
from the API.
Just read the conclusion, it's like you're reading my mind!
But yeah, I think having 2 ways to do the same thing only hurts in any API. I also don't see the benefit. If all examples use qp
as the variable I am A-ok with using qp.<my-value>
since they're using $state it should be fine grained and thus not cause any unwanted updates correct?
I'm currency using the syntax const search = queryParam("search");
but to be honest, I wasn't aware of the other approach. I like the benefits of having more fine-grained control and it would be very simple for us to switch over to when we upgrade to svelte 5.
<script>
import { ssp, queryParameters } from "sveltekit-search-params";
const params = queryParameters({
search: ssp.string(),
});
</script>
@paoloricciuti โ Little plug
I created a library to handles queryParamaters state management in svelte 5. I would love your feedback on it. And maybe you'll find some ideas for your svelte 5 migration.
https://github.com/beynar/kit-state-params
const searchParams = stateParams({
schema: {
search: 'string',
tags: ['number'],
sortBy: '<asc,dec>',
range:{
from:"date",
to:"date"
}
}
});
It does not use the boxed value pattern and behave like a normal svelte 5 state.
Ps: i used your library in a lot of projects, so thank you ๐
@paoloricciuti โ Little plug I created a library to handles queryParamaters state management in svelte 5. I would love your feedback on it. And maybe you'll find some ideas for your svelte 5 migration. https://github.com/beynar/kit-state-params
const searchParams = stateParams({ schema: { search: 'string', tags: ['number'], sortBy: '<asc,dec>', range:{ from:"date", to:"date" } } });It does not use the boxed value pattern and behave like a normal svelte 5 state. Ps: i used your library in a lot of projects, so thank you ๐
Hey just noticed this...i'm actually working on the svelte 5 version right now...i like the added validation and i will definitely take a look at this ๐
@paoloricciuti If you want to add validation maybe it's possible to re-use existing validation libraries like zod?
Would be a shame to reinvent the wheel.
@paoloricciuti If you want to add validation maybe it's possible to re-use existing validation libraries like zod? Would be a shame to reinvent the wheel.
I don't plan to add validations during the svelte 5 conversion but if i will do something similar in the future i will definitely try to make it with some existing library (possibly more than one with the adapter pattern)
Thanks everyone for the feedback...i've opened this PR to close this issue.
There are a few breaking changes (the biggest is the removal of queryParam
and the move of equalityFn
from general options to specific option) but for the rest it should more or less work the same (obviously using runes instead of stores).
You can try it out today by doing
pnpm add https://pkg.pr.new/sveltekit-search-params@126
and i setup a prerelease that i plan to keep around for a bit to gather feedback before pushing to the new Major (probably after sveltekit will introduce the stateful version of the stores).
Please let me know if there are any bugs.
How to achieve the showDefault: false
in the new rune mode ?
The following code always ads ?page=1 in my url, I don't want the default page 1 to appear in my URL.
const params = queryParameters({ page: ssp.number(1) })
How to achieve the
showDefault: false
in the new rune mode ? The following code always ads ?page=1 in my url, I don't want the default page 1 to appear in my URL.
const params = queryParameters({ page: ssp.number(1) })
There's the showDefaults
option as the second argument of the queryParameter
function
Apologies for missing that earlier. I just reviewed the documentation and came back to update my comment, only to realize you already addressed it.
Additionally, I was thinking it might be useful to introduce a resetDefault: true
option. This would ensure that parameters with default values reset when other parameters change.
For instance, if this is used as a search result filter, and the user is on page 20 but applies additional filters that reduce the total number of pages below 20, they would still be on page 20 and see no results.
If you have an even better approach in mind, Iโm all ears!
Before switching to v4, I was able to set different debounce times for individual parameters. However, it seems that this is no longer possible. Iโm using this setup in search forms and would like to apply different debounce behaviors based on the input type:
- No debounce for parameters rendered with elements like
<Select>
,<Checkbox>
, etc. - Debounce only for parameters with
<input type="text">
elements.
Is there a way to achieve this in the current setup, or any workaround youโd recommend?
Hi @paoloricciuti , where would you like to track issues with @126? I am unclear about how we should be reacting to query changes. For example, this seems to cause an infinite number of function calls:
const params = queryParameters({ selected: ssp.array() }, { pushHistory: false })
$effect(() => {
if (params.selected) {
fetchProperties()
}
})
Hi @paoloricciuti , where would you like to track issues with @126? I am unclear about how we should be reacting to query changes. For example, this seems to cause an infinite number of function calls:
const params = queryParameters({ selected: ssp.array() }, { pushHistory: false }) $effect(() => { if (params.selected) { fetchProperties() } })
You can just open an issue in the repo...what are you doing inside fetchProperties
? If you can provide an actual reproduction I'll look into it
Just tried and it seems to work fine
It's probably a poor design decision on my part, but I'm not sure how to get around it: I fetch each item in the array and add it to another $state. There's also some memoization in here to avoid re-fetching items:
let properties = $state([])
const fetchProperties = async () => {
const newProperties = {}
for (const id of $params.selected) {
if (!newProperties[id] && !properties.some((e) => e.id == id) {
newProperties[id] = await fetch(`/api/property/${id}`).then(e => e.json())
}
}
properties = properties
.filter((e) => $params.selected?.some((id) => e.id == id)
.concat(Object.values(newProperties))
}
Wrapping the async potion of this in a setTimeout(0) made the infinte loop go away, but there's still some weird behavior.
Basically, I don't know how to load data based on changes to params. With regular stores, .subscribe()
seems to work well.
I'll add that I appreciate your effort on this, I'm surprised query-string stores aren't a native feature in svelte(kit), it seems like a required feature for deep linking in webapps.
It's probably a poor design decision on my part, but I'm not sure how to get around it: I fetch each item in the array and add it to another $state. There's also some memoization in here to avoid re-fetching items:
let properties = $state([]) const fetchProperties = async () => { const newProperties = {} for (const id of $params.selected) { if (!newProperties[id] && !properties.some((e) => e.id == id) { newProperties[id] = await fetch(`/api/property/${id}`).then(e => e.json()) } } properties = properties .filter((e) => $params.selected?.some((id) => e.id == id) .concat(Object.values(newProperties)) }Wrapping the async potion of this in a setTimeout(0) made the infinte loop go away, but there's still some weird behavior. Basically, I don't know how to load data based on changes to params. With regular stores,
.subscribe()
seems to work well.I'll add that I appreciate your effort on this, I'm surprised query-string stores aren't a native feature in svelte(kit), it seems like a required feature for deep linking in webapps.
This is the source of the infinite loop...you are reading and writing to the same state (properties)...so that will trigger the infinite loop. This has nothing to do with the library tho, you probably just need to untrack
the read since you don't really want the effect to rerun when properties changes.
Is there a prerelease version of svelte 5 that we can try out and give feedback / help contribute? I don't see any pr's
Is there a prerelease version of svelte 5 that we can try out and give feedback / help contribute? I don't see any pr's
Yes you can install the @next
tag
Was a little surprised/confused that $inspect(params)
returns an empty object unless wrapped in JSON.stringify()
.
With query string ?sort=desc
...
const params = queryParameters({ sort: true });
$inspect(params);
returns an empty object {}
but....
const params = queryParameters({ sort: true });
$inspect(JSON.stringify(params));
returns the expected {"sort":"desc"}
.
Am I misunderstanding how $inspect()
/ queryParameters
should work?
Since queryParameters
is a Proxy/state rune, I thought $inspect()
would handle this.
console.log(JSON.stringify(params))
also works, but also needs to be wrapped in an $effect()
to be reactive.