Skip to content

feat: Svelte 5 API proposal #8852

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

Conversation

elliott-with-the-longest-name-on-github
Copy link

See the upstream PR that this is based against. This adds more efficiency and a ton of tests!

zhihengGet and others added 4 commits February 16, 2025 23:27
docs(svelte-5-adapter): force runes mode for all examples (TanStack#7800)

* docs(examples): force runes mode for all examples

* Fix simple example mounting

* Fix type
feat(svelte-5-adapter): require options to be passed as function (TanStack#7804)

* fix(svelte-5-adapter): Require options to be passed as function

* More fixes

* More fixes

* Update examples

fix(svelte-5-adapter): function for `createMutation` options (TanStack#7805)

* fix(svelte-5-adapter): require function for createMutation options

* Fix type errors
refactor(svelte-5-adapter): simplify createBaseQuery (TanStack#7808)

* refactor: simplify createBaseQuery

* Don't take snapshot of queryKey
chore(svelte-5-adapter): tidy-up functions (TanStack#7809)

* chore(svelte-5-adapter): tidy-up functions

* Fix types

* Update svelte

Fix import extensions

chore: ignore state_snapshot_uncloneable in tests


Fix merge
Copy link

nx-cloud bot commented Mar 25, 2025

View your CI Pipeline Execution ↗ for commit ac1ab87.

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 51s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 9s View ↗

☁️ Nx Cloud last updated this comment at 2025-05-12 03:30:40 UTC

autofix-ci bot and others added 3 commits March 25, 2025 07:01
…-the-longest-name-on-github/tanstack-query into svelte-5-adapter-derived-by
@niemyjski
Copy link

Isn't it a bad thing to have so many $effects?

Also how does this work for export scenarios like this where all of my queries are exported https://github.com/exceptionless/Exceptionless/blob/main/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts currently feels weird and I've been trying to rework params...

@elliott-with-the-longest-name-on-github
Copy link
Author

Isn't it a bad thing to have so many $effects?

Can you expand upon what you mean? There aren't many effects here, right?

Effects in and of themselves aren't bad things, they just become bad things if misused. In this case, there's really no way to synchronize state with an external system (@tanstack/query-core) without using them.

Also how does this work for export scenarios like this where all of my queries are exported https://github.com/exceptionless/Exceptionless/blob/main/src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts currently feels weird and I've been trying to rework params...

Basically, you'd just wrap those query exports in $derived.by at the call site and you wouldn't need to pass a function to them for parameters anymore.

const { data, isLoading } = $derived.by(getOrganizationProjectsQuery(...));

@zhihengGet
Copy link

zhihengGet commented Mar 26, 2025

  1. to me, passing function into createQuery feels more natural, solid query also follows the same pattern i believe
  2. easier optimistic updates with $state instead of $derived.by, we can simply mutate the query result .... I know we can assign value to $derived in latest svelte for optimistic update but $state feels easier
const data = createQuery(()=>{ ... }) //passing function and directly mutate data for optimistic updates, not sure if it is the best practice tho
const { data, isLoading, ...etc } = $derived.by( // if u want to spread it... then you need $derived.by, but i prefer not doing it
  data 
);
const { data, isLoading, ...etc } = $derived.by(  // you can't directly mutate data I believe ?  in recent version, we can assign stuff to $derived for optimistic updates but mutate $state feels better? subjective
  createQuery({ queryKey, queryFn })
);

@elliott-with-the-longest-name-on-github
  1. easier optimistic updates with $state instead of $derived.by, we can simply mutate the query result .... I know we can assign value to $derived in latest svelte for optimistic update but $state feels easier
const { data, isLoading, ...etc } = $derived.by(  // you can't directly mutate data I believe ?  in recent version, we can assign stuff to $derived for optimistic updates but mutate $state feels better? subjective
  createQuery({ queryKey, queryFn })
);

As far as optimistic updates go:

  • The current $derived.by design is more efficient than the old one, because the data property being returned is effectively $state.raw -- i.e. not deeply proxified. This means that you can update it by assigning to it at the top level:
    data = { foo: 'bar', ...data } // will work
    data.foo = 'bar' // will not work, data is not a deep proxy
    So maybe it's slightly less convenient in that regard? However, I want to point out, you definitely should not use this strategy for optimistic updates. It is not covered by semver and probably never will be. Your app will probably break unexpectedly at some point because of some patch version that subtly changes how the values are returned, proxied, updated -- whatever. If you need to do optimistic updating, you should use setQueryData, or, if you still want to feel fancy, create your own derived:
    const { data } = $derived.by(createQuery(...))
    const dataIWantToOptimisticallyUpdate = $derived(data.foo.bar)
    
    // later
    dataIWantToOptimisticallyUpdate = 'baz'

@niemyjski
Copy link

Doesn't really matter to me but feels like it's way less idiomatic and I don't like seeing framework code.

@elliott-with-the-longest-name-on-github

I'm curious -- what idioms are you referring to?

The alternative API is basically what we had before:

const query = createQuery(() => ({ queryKey, queryFn }))

Downsides being:

  • Not all arguments are reactive (if you're passing in a custom queryClient and that client changes, it's impossible to make createQuery pick it up)
  • You lose destructuring

As far as idioms go, you basically need a thunk on the way in (to preserve argument reactivity) or on the way out (to preserve return value reactivity) -- but not both. Svelte is designed to handle thunk returns gracefully with $derived.by -- and the core team is spiritually against passing functions as arguments if at all possible -- but if the community at large really does prefer the other way around, well, we could do that 🤷‍♂️

Either way, the optimistic updates thing above applies -- this shouldn't be implemented with data as $state. It's needlessly inefficient and is bug-prone from the library side.

@arnolicious
Copy link

I find the example from proposal very elegant 👍

Avoiding the need to use functions for the arguments is amazing, and since queries do depend on outside variables (like the query key) it does make perfect sense that you'd need to wrap it in a derived (derived.by), like any other derived value.

@niemyjski
Copy link

niemyjski commented Mar 28, 2025

I'm curious -- what idioms are you referring to?

The alternative API is basically what we had before:

const query = createQuery(() => ({ queryKey, queryFn }))

Downsides being:

  • Not all arguments are reactive (if you're passing in a custom queryClient and that client changes, it's impossible to make createQuery pick it up)
  • You lose destructuring

As far as idioms go, you basically need a thunk on the way in (to preserve argument reactivity) or on the way out (to preserve return value reactivity) -- but not both. Svelte is designed to handle thunk returns gracefully with $derived.by -- and the core team is spiritually against passing functions as arguments if at all possible -- but if the community at large really does prefer the other way around, well, we could do that 🤷‍♂️

Either way, the optimistic updates thing above applies -- this shouldn't be implemented with data as $state. It's needlessly inefficient and is bug-prone from the library side.

Having $derived.by at every callsite doesn't feel like just writing javascript (idomatic). And I agree I don't like passing functions either (which seems worse) and not intuitive on what should be a function or not. I have not been destructuring at all here because I haven't seen anyone else do it for tanstack nor the docs (unless they've been updated). I'm always concerned about broken references e.g., isSuccess (computed) being updated properly when destructured?

I agree with this as well:

Avoiding the need to use functions for the arguments is amazing, and since queries do depend on outside variables (like the query key) it does make perfect sense that you'd need to wrap it in a derived (derived.by), like any other derived value.

On final note, I'm fine with what ever design but for the sake of porting docs / and samples would be great to have the least amount of changes as possible as svelte doesn't get much love from tanstack docs/community updating those (And I get I could contribute there as well, time is very very limited).

Thanks again!

@elliott-with-the-longest-name-on-github
Copy link
Author

Having $derived.by at every callsite doesn't feel like just writing javascript (idomatic).

I just want to point out that "every callsite" is not technically correct, though I expect what you mean is probably true. This would absolutely work:

const query = createQuery({ queryKey, queryFn })

...you just can't destructure it, and queryKey and queryFn can't be updated to new values. So it's a very constrained usecase.

More practically, though, you definitely don't need it when creating your own queries:

function createUserQuery({ id }) {
  return createQuery({ 
    queryKey: ['user', id], 
    queryFn: ({ queryKey }) => fetch(`/api/users/${queryKey[1]}`).then(r => r.json)
  })
}

...you would just need it where you actually use the query:

const { data, isLoading } = $derived.by(
  createUserQuery({ id })
)

I have not been destructuring at all here because I haven't seen anyone else do it for tanstack nor the docs (unless they've been updated). I'm always concerned about broken references e.g., isSuccess (computed) being updated properly when destructured?

To answer the surface-level question, so long as you're using $derived.by, yes, you can destructure and all of the values will remain in sync exactly as you'd expect them to. To explain why on the deeper level: $derived.by is just kinda awesome. Basically what it says is "give me a function, and I'll rerun it whenever any of its dependencies change". createQuery returns a function (() => CreateQueryResult<>), which means any time the dependencies of that function change, it'll rerun and give you the latest value -- which is then destructured and assigned to the variables you've declared.

And I agree I don't like passing functions either (which seems worse) and not intuitive on what should be a function or not.

This has been the beast I've had to struggle with, especially the intuitiveness part. Maybe for some insanely-bleeding-edge, maximum-possible-performance-needed scenarios it makes sense to fully take advantage of fine-grained reactivity by passing individual getters or functions across the hook boundary, but Tanstack Query is not that use case, especially since all of its state is already externally hosted in @tanstack/query-core. The simplest mental model is "Hey, when any of the things this current query depends upon change, I need to create a new query" -- which is exactly what $derived does. (And, for what it's worth, it's still really fast 😁)

I'm fine with what ever design but for the sake of porting docs / and samples would be great to have the least amount of changes as possible as svelte doesn't get much love from tanstack docs/community updating those (And I get I could contribute there as well, time is very very limited).

Yeah definitely, this won't go live without updated docs and examples. I'm actually working on porting all of the React tests as well, as the current coverage is pretty abysmal.

Copy link
Member

@lachlancollins lachlancollins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for your work @elliott-with-the-longest-name-on-github ! After a fair bit of thinking, I agree this syntax is the more svelte v5-appropriate choice. @arnolicious 's comment sums it up well:

Avoiding the need to use functions for the arguments is amazing, and since queries do depend on outside variables (like the query key) it does make perfect sense that you'd need to wrap it in a derived (derived.by), like any other derived value.

Giving a big 👍 to keep working on this!

Also I think it would be more appropriate for the PR to target the TanStack:svelte-5-adapter branch, so we're comparing the two runes implementations.

@elliott-with-the-longest-name-on-github

I'll re-target the branch -- I had it pointing at main because the diffs were super confusing 😆

@Thiagolino8
Copy link

How will using variables in queryKey work without createQuery accepting a callback?
In the version with the callback it was simple:

let search = $state('')

const query = createQuery(() => ({
  queryKey: ['products', search],
  queryFn: async () => {
    const data = await fetch(`https://dummyjson.com/products/search?q=${search}`).then((r) => r.json())
    return data.products
})

But in this version I couldn't make it work

@hmnd
Copy link

hmnd commented Mar 31, 2025

How will using variables in queryKey work without createQuery accepting a callback?
In the version with the callback it was simple:

let search = $state('')

const query = createQuery(() => ({
  queryKey: ['products', search],
  queryFn: async () => {
    const data = await fetch(`https://dummyjson.com/products/search?q=${search}`).then((r) => r.json())
    return data.products
})

But in this version I couldn't make it work

Since createQuery now returns a thunk (() => query), you would use it just as you would with the mainline branch and wrap it in $derived.by:

const query = $derived.by(createQuery({
   queryKey: ['products', search],
   queryFn: async () => {
     const data = await fetch(`https://dummyjson.com/products/search?q=${search}`).then((r) => r.json())
     return data.products
 }));

@Thiagolino8
Copy link

Thiagolino8 commented Mar 31, 2025

The problem is not how to use the result of createQuery, but how to make it reactive to the properties of the object it receives as a parameter
Even the svelte compiler warns that this way doesn't work
Here I am using the version from the last commit of this PR
Code_LDYezKJIJF

@hmnd
Copy link

hmnd commented Mar 31, 2025

@Thiagolino8 oh I see... Yeah, that's a problem. You can get around it by passing your own thunk to $derived.by, so you'd end up with $derived.by(() => createQuery({...})), but that doesn't feel very intuitive..

@Thiagolino8
Copy link

You can get around it by passing your own thunk to $derived.by, so you'd end up with $derived.by(() => createQuery({...}))

This would recreate the query on every state change
Definitely not the ideal

@hmnd
Copy link

hmnd commented Mar 31, 2025

In this case, I feel like the original solution of passing the query options as a thunk might be more ideal.

Another alternative I can think of would be to support passing a thunk to queryKey and/or values as thunks within the queryKey array. But then using the values in queryFn feels sort of disjointed.

Given that the solid adapter already sets a precedent for passing a thunk to createQuery, I feel that might be the superior solution after all. There is also precedent among Svelte maintainers that this is the way to go for maintaining reactivity.

@jthayworth
Copy link

jthayworth commented Mar 31, 2025 via email

@elliott-with-the-longest-name-on-github

FYI to anyone watching, if you end up with a couple of spare hours and want to help out, you can port some tests! I'm using the Solid ones because they're very similar to Svelte, and you can follow along with the createQuery.svelte.test.ts file for how to set them up. Just leave a comment here with which file you're brining over and I'll avoid picking it up.

Copy link

pkg-pr-new bot commented Apr 12, 2025

More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@8852

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@8852

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@8852

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@8852

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@8852

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@8852

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@8852

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@8852

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@8852

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@8852

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@8852

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@8852

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@8852

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@8852

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@8852

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@8852

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@8852

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@8852

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@8852

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@8852

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@8852

commit: ac1ab87

Copy link
Member

@lachlancollins lachlancollins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elliott-with-the-longest-name-on-github when you get a chance, can you target the PR against TanStack:svelte-5-adapter? I've just rebased that branch onto TanStack:main (it was a few months behind), so there should hopefully be fewer conflicts.

@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github changed the base branch from main to svelte-5-adapter April 15, 2025 23:29
@elliott-with-the-longest-name-on-github

@lachlancollins apologies for the delay -- I'm actually prepping for my wedding so it's a hectic time 😆 I got it rebased and un-conflicted with your branch -- I think it's pretty well ready to go save for the tests -- there are still a bunch to port over from the Solid library. I'm hoping to be able to bash those out tomorrow and the day after.

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Apr 30, 2025
@firatciftci
Copy link

Hi all, I have been following along your amazing work here and thank you for all your hard work! I just saw that the docs were updated to have correct examples of the new API proposal. Would it be possible to also add an example about how a reactive variable with $state() being used as a query key would work? Do we simply need to wrap the newly-updated createQuery(() => ...) function inside a $derived? Are there any footguns that we need to be aware of?

@hmnd
Copy link

hmnd commented Apr 30, 2025

Hi all, I have been following along your amazing work here and thank you for all your hard work! I just saw that the docs were updated to have correct examples of the new API proposal. Would it be possible to also add an example about how a reactive variable with $state() being used as a query key would work? Do we simply need to wrap the newly-updated createQuery(() => ...) function inside a $derived? Are there any footguns that we need to be aware of?

@firatciftci passing in the thunk (() => ...) makes everything within it reactive, so that's all you would need to do for the queryKey to react to state!

@lachlancollins lachlancollins marked this pull request as ready for review May 12, 2025 02:32
Copy link

codecov bot commented May 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 43.56%. Comparing base (367c06b) to head (ac1ab87).
Report is 478 commits behind head on svelte-5-adapter.

Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                  @@
##           svelte-5-adapter    #8852      +/-   ##
====================================================
- Coverage             44.15%   43.56%   -0.59%     
====================================================
  Files                   171      184      +13     
  Lines                  6994     7894     +900     
  Branches               1574     1749     +175     
====================================================
+ Hits                   3088     3439     +351     
- Misses                 3546     4032     +486     
- Partials                360      423      +63     
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental 88.08% <ø> (+1.50%) ⬆️
@tanstack/eslint-plugin-query 88.19% <ø> (+1.88%) ⬆️
@tanstack/query-async-storage-persister 43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental ∅ <ø> (∅)
@tanstack/query-codemods 0.00% <ø> (ø)
@tanstack/query-core 94.77% <ø> (+1.94%) ⬆️
@tanstack/query-devtools 3.61% <ø> (-1.64%) ⬇️
@tanstack/query-persist-client-core 73.46% <ø> (+15.73%) ⬆️
@tanstack/query-sync-storage-persister 84.61% <ø> (+2.11%) ⬆️
@tanstack/react-query 95.42% <ø> (+2.92%) ⬆️
@tanstack/react-query-devtools 10.00% <ø> (-0.72%) ⬇️
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 78.48% <ø> (+0.27%) ⬆️
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query ∅ <ø> (∅)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client ∅ <ø> (∅)
@tanstack/vue-query 71.01% <ø> (-0.42%) ⬇️
@tanstack/vue-query-devtools ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@lachlancollins lachlancollins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elliott-with-the-longest-name-on-github I've made a few changes here, with the goal to merge this current work into svelte-5-adapter, and keep working on that branch rather than this PR.

I've added back the old tests which haven't yet been migrated. This identified issues with createMutation and useMutationState, so I've reverted these changes for now.

I also noticed that test-d files weren't being run by the split client/server tests, and this was also generating a bunch of document missing errors once these files were re-tested, so I've reverted this split testing for now.

Thanks heaps for all the work you've put in so far, it's certainly getting close to release now!

@lachlancollins lachlancollins merged commit b5f5fd4 into TanStack:svelte-5-adapter May 12, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants