sveltekit-search-params icon indicating copy to clipboard operation
sveltekit-search-params copied to clipboard

Svelte 5 support

Open paoloricciuti opened this issue 1 year ago • 27 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.

paoloricciuti avatar Jun 06 '24 14:06 paoloricciuti

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?

Hugos68 avatar Jun 06 '24 15:06 Hugos68

I agree with @Hugos68 i don't think it's needed.

stolinski avatar Jun 06 '24 23:06 stolinski

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>

lucaswalter avatar Jun 13 '24 19:06 lucaswalter

@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 🙏

beynar avatar Oct 16 '24 11:10 beynar

@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 avatar Oct 28 '24 21:10 paoloricciuti

@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.

tcurdt avatar Oct 28 '24 21:10 tcurdt

@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)

paoloricciuti avatar Oct 28 '24 22:10 paoloricciuti

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.

paoloricciuti avatar Oct 29 '24 10:10 paoloricciuti

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) })

ak4zh avatar Nov 02 '24 07:11 ak4zh

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

paoloricciuti avatar Nov 02 '24 08:11 paoloricciuti

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!

ak4zh avatar Nov 02 '24 08:11 ak4zh

@paoloricciuti!

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?

ak4zh avatar Nov 05 '24 06:11 ak4zh

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()
  }
})

devinellis avatar Nov 10 '24 01:11 devinellis

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

paoloricciuti avatar Nov 10 '24 08:11 paoloricciuti

Just tried and it seems to work fine

https://www.sveltelab.dev/amzeoot1b0l7bq4

paoloricciuti avatar Nov 10 '24 19:11 paoloricciuti

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.

devinellis avatar Nov 10 '24 20:11 devinellis

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.

paoloricciuti avatar Nov 10 '24 20:11 paoloricciuti

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

niemyjski avatar Dec 15 '24 19:12 niemyjski

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

paoloricciuti avatar Dec 15 '24 20:12 paoloricciuti

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.

techniq avatar Jan 03 '25 16:01 techniq

I think I came across a bug today, but I'm not sure if it's existing as I only started with it recently on my svelte 5 SPA app.

But the scenario is that pressing back (history via popstate) doesn't update the params or trigger derived.

    const params = queryParameters({ filter: ssp.string() }, { debounceHistory: 1, pushHistory: true, showDefaults: false });
    const blah = $derived(params.filter);
{blah}

I've also tried

    const params = queryParameters({ filter: ssp.string() });
    const blah = $derived(params.filter);
{blah}

niemyjski avatar Feb 04 '25 02:02 niemyjski

@paoloricciuti any idea why the library doesn't update for popstate? That seems like a big gap

niemyjski avatar Feb 04 '25 15:02 niemyjski

@paoloricciuti any idea why the library doesn't update for popstate? That seems like a big gap

The library is deriving from sveltekit store...if $page update and this library don't than it's a bug

paoloricciuti avatar Feb 04 '25 15:02 paoloricciuti

Bug on sveltekit side or not, we could address it via eventing in the short term. I made a comment over there, might need to create an issue. Hard to believe no one here has ran into this especially with users clicking the back button and then params not being in sync.

niemyjski avatar Feb 04 '25 19:02 niemyjski

Bug on sveltekit side or not, we could address it via eventing in the short term. I made a comment over there, might need to create an issue. Hard to believe no one here has ran into this especially with users clicking the back button and then params not being in sync.

Introducing a different way of synchronizing with the url is out of the scope of this library...if this is a bug in sveltekit is were we need to fix it and I will do it there. Can you provide a reproduction?

paoloricciuti avatar Feb 04 '25 20:02 paoloricciuti

I am seeing this behavior in my app but not in the repo sample I created. I'll have to dig deeper.

niemyjski avatar Feb 08 '25 02:02 niemyjski

Hey @paoloricciuti Are there any updates on the Svelte 5 version?

fromaline avatar Aug 13 '25 09:08 fromaline