Skip to content

feat(svelte-query): Svelte Query Adapter for TanStack Query#4344

Closed
drejohnson wants to merge 33 commits intoTanStack:mainfrom
drejohnson:svelte-adapter
Closed

feat(svelte-query): Svelte Query Adapter for TanStack Query#4344
drejohnson wants to merge 33 commits intoTanStack:mainfrom
drejohnson:svelte-adapter

Conversation

@drejohnson
Copy link
Copy Markdown

@drejohnson drejohnson commented Oct 19, 2022

This pull request adds Svelte adapter for TanStack Query.

Primitives included in the package are:

  • useQueryClient
  • useQuery
  • userQueries
  • useMutation
  • useInfiniteQuery
  • useHydrate
  • useIsMutating
  • useIsFetching

A few examples have been included as well as a few initial tests for useQuery

TODO

  • Add comprehensive test
  • Add docs and readme
  • Add more examples

Acknowledgment

Thanks to:

@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci bot commented Oct 19, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit bdfeac4:

Sandbox Source
@tanstack/query-example-react-basic-typescript Configuration
@tanstack/query-example-solid-basic-typescript Configuration
@tanstack/query-example-svelte-basic Configuration
@tanstack/query-example-vue-basic Configuration

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Oct 22, 2022

please add the basic example to ci.json so that we get preview builds:

"sandboxes": ["/examples/react/basic-typescript", "/examples/solid/basic-typescript", "/examples/vue/basic"],

Copy link
Copy Markdown
Collaborator

@TkDodo TkDodo left a comment

Choose a reason for hiding this comment

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

wow, thanks for this ❤️

as you said, tests are a bit missing. Maybe you could try to add the same test suite that we have for react/solid adapters?

Copy link
Copy Markdown
Contributor

@DamianOsipiuk DamianOsipiuk left a comment

Choose a reason for hiding this comment

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

Looks great 🚀

The only thing i would suggest is to rename basic-typescript to basic example so it could be showcased on the https://tanstack.com/query/v4 as embedded sandbox

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

Base: 96.36% // Head: 90.72% // Decreases project coverage by -5.63% ⚠️

Coverage data is based on head (bdfeac4) compared to base (eab6e2c).
Patch has no changes to coverable lines.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4344      +/-   ##
==========================================
- Coverage   96.36%   90.72%   -5.64%     
==========================================
  Files          45       97      +52     
  Lines        2281     3719    +1438     
  Branches      640      911     +271     
==========================================
+ Hits         2198     3374    +1176     
- Misses         80      326     +246     
- Partials        3       19      +16     
Impacted Files Coverage Δ
src/core/infiniteQueryBehavior.ts
src/core/utils.ts
src/core/queriesObserver.ts
src/core/query.ts
src/react/setLogger.ts
src/core/queryObserver.ts
src/core/retryer.ts
src/devtools/tests/utils.tsx
src/core/mutation.ts
src/core/queryClient.ts
... and 132 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

Copy link
Copy Markdown

@SomaticIT SomaticIT left a comment

Choose a reason for hiding this comment

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

Hello,

Thank you for your amazing work.
I took a little time to review your work and it looks promising.

I added some comments with my first thoughts.
I will try to take more time to help on the migration.

Thanks,


export function useQueryClient(): QueryClient {
let queryClient!: QueryClient
client.subscribe((val) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We are creating a subscription each time we call this method.
The subscription is never disposed and the queryClient is never unmount.
It could leed to a big memory leak and unexpected behaviors.

In the legacy svelte-query, this is managed by the QueryClientProvider which is bound to a svelte Component.

This way, the lifetime of the queryClient is managed by svelte directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the feedback. My initial approach was using a svelte component but I can't get the build system to package .svelte files. I really like using sveltekit's package script. It makes packaging svelte libraries seamless.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think the best approach is indeed using a svelte component.

Another approach, maybe easier, could be to use onMount and onDestroy hooks.
However this means that the useQueryClient will need to be called inside a Svelte component.

Copy link
Copy Markdown

@SomaticIT SomaticIT Nov 5, 2022

Choose a reason for hiding this comment

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

Finally, maybe the best solution would be to use getContext and setContext.

In this case:

  1. we export a method initQueryClient(options) which allows to create a QueryClient with custom options and call setContext to provide the QueryClient to child components. It is also responsible of the management of the QueryClient lifecycle by registering on the onMount and onDestroy hooks to mount/unmount the QueryClient.
  2. we change useQueryClient() to use getContext to retrieve the current QueryClient

This approach has multiple advantages:

  • allow to provide custom options
  • allow to have multiple QueryClient with different options
  • avoid having a static global QueryClient
  • avoid creating "ghost" subscriptions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On "ghost subscriptions", I've been trying this PR out in one of our projects and found one design decision that we might need to rethink, unless your suggestion solves this as well:

Here's the scenario:

let myQuery = useQuery({enabled: false, queryFn: => (....)}) — let's say queryFn depends on some $reactive variables, so when useQuery is called all of them are undefined, so we disable it by default or else the query would fail the first time. We actually want the query to run each time one of those vars changes as they are probably bound to a user input.

Unlike in a React component, useQuery will only ever be called once when the component is initialized. We could assigned it reactively e.g. $: myQuery = useQuery(...), which would wait until the dependent variables are initialized and it would appear to work, however, the observer and subscriptions would be recreated each time leading to a memory leak.

One workaround would be to change the API so that the query is initialized separately from actually being called:

let myQuery = useQuery({queryFn: ({variables})=> {...}})
$: myQuery.query({variables: {a: $aVar}})

This way the queryFn can receive the inputs it needs when they change, triggering a key change and query.

Another idea: make queryFn a store, so any reactive change would trigger a query.

client.subscribe((val) => {
queryClient = val
queryClient.mount()
return () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Returning a function in a svelte subscription does nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

According to the docs it does:

The .subscribe method must return an unsubscribe function. Calling an unsubscribe function must stop its subscription, and its corresponding subscription function must not be called again by the store.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The doc you are mentioning is about the Store contract, it explains how to manually create a store.

The quote says that the .subscribe method an unsubscribe method. It does not mention the callback that is provided to the .subscribe function.

You can try it easily:

import { writable } from "svelte/store";

const store = writable(1);

const unsubscribe = store.subscribe((val) => {
  console.log("store value", val);
  return () => console.log("unsubscribe callback");
});

console.log("subscribed");

unsubscribe();
console.log("unsubscribed");

/*
console outputs:
store value 1
subscribed
unsubscribed
*/

As you can see the unsubscribe callback is never called.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interesting. Thanks for testing that out, I'm quite surprised by that behaviour.

: $result
})

return { subscribe }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In SvelteStack/svelte-query, we also have additional methods: setOptions, updateOptions, setEnabled.
These methods were very useful in realworld scenarios.

Is it possible to implement them?

@ivanvanderbyl
Copy link
Copy Markdown

This looks great, thanks for making it happen, I'm very excited to use this ❤️

I'd like to suggest an API change to make it feel more Svelte-like. The prefix of use is really a React thing, implying that you're going to receive a hook. This isn't a concept in Svelte since rendering doesn't happen by calling a function to emit DOM nodes (e.g React) and it makes the API feel a bit unconventional to Svelte users.

In Svelte, when working with data, most of the operations are either setting/getting, either as state or subscribing/updating a store, donated by the $ prefix.

For example, the equivalent of useContext in Svelte is setContext and getContext and it does exactly as it says on the tin, and when working with a readable such as let store = readable(...) it will be accessed with $store. Whereas useQuery() could simply be query() or queryStore(), or useIsMutating could be $isMutating.

One of my favourite examples of this is Urql. The React API uses useQuery and useMutation and Svelte API uses queryStore and mutationStore.

What do you think?

@drejohnson
Copy link
Copy Markdown
Author

Sorry guys, I've been on vacation. I'll update the PR sometime this week to get this moving along

@drejohnson
Copy link
Copy Markdown
Author

This is looking great! Thanks for adding the examples. I'm not familiar with Svelte as much but seems like their reactivity model is similar to Solid. Let me know if you need any help getting the tests we wrote for solid ported to the svelte-query package :D

Thanks, I may have to take you up on your offer

@drejohnson
Copy link
Copy Markdown
Author

@drejohnson anything I can help with to move this along?

Thanks! Are you familair with rollup? I can't figure out how to package .svelte components. That and writing tests :D

@drejohnson
Copy link
Copy Markdown
Author

This looks great, thanks for making it happen, I'm very excited to use this ❤️

I'd like to suggest an API change to make it feel more Svelte-like. The prefix of use is really a React thing, implying that you're going to receive a hook. This isn't a concept in Svelte since rendering doesn't happen by calling a function to emit DOM nodes (e.g React) and it makes the API feel a bit unconventional to Svelte users.

In Svelte, when working with data, most of the operations are either setting/getting, either as state or subscribing/updating a store, donated by the $ prefix.

For example, the equivalent of useContext in Svelte is setContext and getContext and it does exactly as it says on the tin, and when working with a readable such as let store = readable(...) it will be accessed with $store. Whereas useQuery() could simply be query() or queryStore(), or useIsMutating could be $isMutating.

One of my favourite examples of this is Urql. The React API uses useQuery and useMutation and Svelte API uses queryStore and mutationStore.

What do you think?

Yeah, I agree, I initially thought about doing this.

@SomaticIT
Copy link
Copy Markdown

Good changes,

I have some feedbacks:

  1. getQueryClientContext and setQueryClientContext should not be exported in index.ts
  2. store.ts is not needed anymore
  3. useQueryClient should not be responsible of mount/unmount. It should be done directly in setQueryClient. useQueryClient is just a shortcut to getQueryClientContext
  4. we should implement methods: setOptions, updateOptions, setEnabled on BaseQuery and on all children classes

What do you think?

export const setQueryClient = (config?: QueryClientConfig): QueryClient => {
const client = new QueryClient(config)
setQueryClientContext(client)
onMount(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍🏻

@emarj
Copy link
Copy Markdown

emarj commented Dec 4, 2022

Any update on this?

@lachlancollins
Copy link
Copy Markdown
Member

Hi all, I'm wondering if it would be possible to merge this PR without releasing the package on NPM just yet? It seems like the core functionality is all here (@drejohnson has done an amazing job!), but the final work might benefit from allowing others to contribute their own PRs in this repository.

I think it would be awesome to have this released close to the SvelteKit 1.0 release (ETA around 5 days), and I'm sure a lot of people trying it out from a React background would be happy to see TanStack Query available!

Based on this thread, it seems the final things that are still to-do are fixing some memory leaks, considering renaming exported functions (e.g. useQuery vs createQuery vs queryStore), adding some missing methods, and more tests.

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Dec 10, 2022

@lachlancollins we could also make PRs to this PR ?

we could merge the PR and not release it, but any further PR that gets merged to main that has a fix or feat commit would release it automatically. This would severely hinder us in development. Also, the failing tests would block any further merges.

So if you want to get this merged, please contribute to this PR.

@romelperez
Copy link
Copy Markdown

romelperez commented Dec 17, 2022

Hello! Is it possible to have a list of the specific tasks that are needed to complete for this PR to be merged? I see there are 3 todo items in the description but I would like more information so I can contribute if possible. Thank you!

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Dec 30, 2022

This PR is stalled a bit, but we'd really like to get it over the finish line. I honestly don't know what is missing to get that done, mainly, the question would be if the author @drejohnson is still around to finish it, or if someone else would pick it up?

What we would need is someone to own the svelte-query adapter and who's willing to stick around to maintain it after the first release. We're also working on v5 already and I wouldn't want to merge this to main without someone being able to help with v5 work.

@lachlancollins
Copy link
Copy Markdown
Member

@lachlancollins we could also make PRs to this PR ?

we could merge the PR and not release it, but any further PR that gets merged to main that has a fix or feat commit would release it automatically. This would severely hinder us in development. Also, the failing tests would block any further merges.

So if you want to get this merged, please contribute to this PR.

Hi @TkDodo, thanks for your reply - when I want to merge my changes into this current PR, do I need to make a new PR into TanStack/query or into drejohnson/query? Thanks!

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Jan 5, 2023

@lachlancollins depends on what is easier for you. If you think it's starting from scratch, I'll close this PR and you can copy the commits over to a new PR. If you want to make a PR to this PR that's also fine for me.

@smblee
Copy link
Copy Markdown

smblee commented Jan 5, 2023

Im willing to contribute in any way possible as well. Let me know if you want to divide and conquer this @lachlancollins

@lachlancollins
Copy link
Copy Markdown
Member

Hi @TkDoko, I've just made a new PR at #4768. I think it might be easier to use a new PR so that we don't have to make multiple PRs here for each change.

@smblee I would definitely appreciate some help! Please have a look at the to-do list as a starting point :)

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Jan 7, 2023

superseded by #4768

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.