Skip to content

feat(svelte-query): use store for reactivity in options #4995

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/svelte/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ Svelte Query offers useful functions and components that will make managing serv
Svelte Query offers an API similar to React Query, but there are some key differences to be mindful of.

- Many of the functions in Svelte Query return a Svelte store. To access values on these stores reactively, you need to prefix the store with a `$`. You can learn more about Svelte stores [here](https://svelte.dev/tutorial/writable-stores).
- If your query or mutation depends on variables, you must assign it reactively. You can read more about this [here](./reactivity).
- If your query or mutation depends on variables, you must use a store for the options. You can read more about this [here](./reactivity).
21 changes: 12 additions & 9 deletions docs/svelte/reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ id: reactivity
title: Reactivity
---

Svelte uses a compiler to build your code which optimises rendering. By default, variables will run once, unless they are referenced in your markup. To make a different variable or function reactive, you need to use [reactive declarations](https://svelte.dev/tutorial/reactive-declarations). This also applies to Svelte Query.
Svelte uses a compiler to build your code which optimises rendering. By default, variables will run once, unless they are referenced in your markup. To be able to react to changes in options you need to use [stores](https://svelte.dev/tutorial/writable-stores).

In the below example, the `refetchInterval` option is set from the variable `intervalMs`, which is edited by the input field. However, as the query is not told it should react to changes in `intervalMs`, `refetchInterval` will not change when the input value changes.

```markdown
<script lang="ts">
<script>
import { createQuery } from '@tanstack/svelte-query'

let intervalMs = 1000
Expand All @@ -25,22 +25,25 @@ In the below example, the `refetchInterval` option is set from the variable `int
<input bind:value={intervalMs} type="number" />
```

To solve this, you can prefix the query with `$: ` to tell the compiler it should be reactive.
To solve this, create a store for the options and use it as input for the query. Update the options store when the value changes and the query will react to the change.

```markdown
<script lang="ts">
<script>
import { createQuery } from '@tanstack/svelte-query'

let intervalMs = 1000

const endpoint = 'http://localhost:5173/api/data'

$: query = createQuery({
const queryOptions = writable({
queryKey: ['refetch'],
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
refetchInterval: intervalMs,
refetchInterval: 1000,
})
const query = createQuery(queryOptions)

function updateRefetchInterval(event) {
$queryOptions.refetchInterval = event.target.valueAsNumber
}
</script>

<input bind:value={intervalMs} type="number" />
<input type="number" on:input={updateRefetchInterval} />
```
2 changes: 1 addition & 1 deletion packages/svelte-query/src/__tests__/CreateQueries.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { setQueryClientContext } from '../context'
import type { QueriesOptions } from '../createQueries'

export let options: { queries: readonly [...QueriesOptions<any>] }
export let options: { queries: [...QueriesOptions<any>] }

const queryClient = new QueryClient()
setQueryClientContext(queryClient)
Expand Down
10 changes: 8 additions & 2 deletions packages/svelte-query/src/__tests__/CreateQuery.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import { createQuery, QueryClient, type CreateQueryOptions } from '../index'
import { createQuery, QueryClient, type CreateQueryOptions, type WritableOrVal } from '../index'
import { setQueryClientContext } from '../context'

export let options: CreateQueryOptions<any>
export let options: WritableOrVal<CreateQueryOptions<any>>

const queryClient = new QueryClient()
setQueryClientContext(queryClient)
Expand All @@ -17,3 +17,9 @@
{:else if $query.isSuccess}
<p>Success</p>
{/if}

<ul>
{#each $query.data ?? [] as entry}
<li>id: {entry.id}</li>
{/each}
</ul>
36 changes: 36 additions & 0 deletions packages/svelte-query/src/__tests__/createQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, it, expect } from 'vitest'
import { render, waitFor } from '@testing-library/svelte'
import { writable } from 'svelte/store'
import CreateQuery from './CreateQuery.svelte'
import { sleep } from './utils'
import type { CreateQueryOptions, WritableOrVal } from '../types'

describe('createQuery', () => {
it('Render and wait for success', async () => {
Expand All @@ -25,4 +27,38 @@ describe('createQuery', () => {
expect(rendered.getByText('Success')).toBeInTheDocument()
})
})

it('should keep previous data when returned as placeholder data', async () => {
const options: WritableOrVal<CreateQueryOptions> = writable({
queryKey: ['test', [1]],
queryFn: async ({ queryKey }) => {
await sleep(50)
const ids = queryKey[1]
if (!ids || !Array.isArray(ids)) return []
return ids.map((id) => ({ id }))
},
placeholderData: (previousData: { id: number }[]) => previousData,
})
const rendered = render(CreateQuery, { props: { options } })

expect(rendered.queryByText('id: 1')).not.toBeInTheDocument()
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()

await sleep(100)

expect(rendered.queryByText('id: 1')).toBeInTheDocument()
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()

options.update((o) => ({ ...o, queryKey: ['test', [1, 2]] }))

await sleep(0)

expect(rendered.queryByText('id: 1')).toBeInTheDocument()
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()

await sleep(100)

expect(rendered.queryByText('id: 1')).toBeInTheDocument()
expect(rendered.queryByText('id: 2')).toBeInTheDocument()
})
})
79 changes: 43 additions & 36 deletions packages/svelte-query/src/createBaseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
import { notifyManager } from '@tanstack/query-core'
import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types'
import type {
CreateBaseQueryOptions,
CreateBaseQueryResult,
WritableOrVal,
} from './types'
import { useQueryClient } from './useQueryClient'
import { derived, readable } from 'svelte/store'
import { derived, get, readable, writable } from 'svelte/store'
import { isWritable } from './utils'

export function createBaseQuery<
TQueryFnData,
Expand All @@ -11,61 +16,63 @@ export function createBaseQuery<
TQueryData,
TQueryKey extends QueryKey,
>(
options: CreateBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
options: WritableOrVal<
CreateBaseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
>,
Observer: typeof QueryObserver,
queryClient?: QueryClient,
): CreateBaseQueryResult<TData, TError> {
const client = useQueryClient(queryClient)
const defaultedOptions = client.defaultQueryOptions(options)
defaultedOptions._optimisticResults = 'optimistic'

let observer = new Observer<
const optionsStore = isWritable(options) ? options : writable(options)

const defaultedOptionsStore = derived(optionsStore, ($options) => {
const defaultedOptions = client.defaultQueryOptions($options)
defaultedOptions._optimisticResults = 'optimistic'

// Include callbacks in batch renders
if (defaultedOptions.onError) {
defaultedOptions.onError = notifyManager.batchCalls(
defaultedOptions.onError,
)
}

if (defaultedOptions.onSuccess) {
defaultedOptions.onSuccess = notifyManager.batchCalls(
defaultedOptions.onSuccess,
)
}

if (defaultedOptions.onSettled) {
defaultedOptions.onSettled = notifyManager.batchCalls(
defaultedOptions.onSettled,
)
}

return defaultedOptions
})

const observer = new Observer<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>(client, defaultedOptions)

// Include callbacks in batch renders
if (defaultedOptions.onError) {
defaultedOptions.onError = notifyManager.batchCalls(
defaultedOptions.onError,
)
}

if (defaultedOptions.onSuccess) {
defaultedOptions.onSuccess = notifyManager.batchCalls(
defaultedOptions.onSuccess,
)
}

if (defaultedOptions.onSettled) {
defaultedOptions.onSettled = notifyManager.batchCalls(
defaultedOptions.onSettled,
)
}
>(client, get(defaultedOptionsStore))

readable(observer).subscribe(($observer) => {
observer = $observer
defaultedOptionsStore.subscribe(($defaultedOptions) => {
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
observer.setOptions(defaultedOptions, { listeners: false })
observer.setOptions($defaultedOptions, { listeners: false })
})

const result = readable(observer.getCurrentResult(), (set) => {
return observer.subscribe(notifyManager.batchCalls(set))
})

const { subscribe } = derived(result, ($result) => {
$result = observer.getOptimisticResult(defaultedOptions)
return !defaultedOptions.notifyOnChangeProps
$result = observer.getOptimisticResult(get(defaultedOptionsStore))
return !get(defaultedOptionsStore).notifyOnChangeProps
? observer.trackResult($result)
: $result
})
Expand Down
15 changes: 9 additions & 6 deletions packages/svelte-query/src/createInfiniteQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InfiniteQueryObserver } from '@tanstack/query-core'
import type {
CreateInfiniteQueryOptions,
CreateInfiniteQueryResult,
WritableOrVal,
} from './types'
import { createBaseQuery } from './createBaseQuery'

Expand All @@ -17,12 +18,14 @@ export function createInfiniteQuery<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey
options: WritableOrVal<
CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey
>
>,
queryClient?: QueryClient,
): CreateInfiniteQueryResult<TData, TError> {
Expand Down
20 changes: 13 additions & 7 deletions packages/svelte-query/src/createMutation.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
import { readable, derived } from 'svelte/store'
import { readable, derived, writable, get } from 'svelte/store'
import type { QueryClient, RegisteredError } from '@tanstack/query-core'
import { MutationObserver, notifyManager } from '@tanstack/query-core'
import type {
CreateMutateFunction,
CreateMutationOptions,
CreateMutationResult,
WritableOrVal,
} from './types'
import { useQueryClient } from './useQueryClient'
import { isWritable } from './utils'

export function createMutation<
TData = unknown,
TError = RegisteredError,
TVariables = void,
TContext = unknown,
>(
options: CreateMutationOptions<TData, TError, TVariables, TContext>,
options: WritableOrVal<
CreateMutationOptions<TData, TError, TVariables, TContext>
>,
queryClient?: QueryClient,
): CreateMutationResult<TData, TError, TVariables, TContext> {
const client = useQueryClient(queryClient)
let observer = new MutationObserver<TData, TError, TVariables, TContext>(

const optionsStore = isWritable(options) ? options : writable(options)

const observer = new MutationObserver<TData, TError, TVariables, TContext>(
client,
options,
get(optionsStore),
)
let mutate: CreateMutateFunction<TData, TError, TVariables, TContext>

readable(observer).subscribe(($observer) => {
observer = $observer
optionsStore.subscribe(($options) => {
mutate = (variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
}
observer.setOptions(options)
observer.setOptions($options)
})

const result = readable(observer.getCurrentResult(), (set) => {
Expand Down
26 changes: 13 additions & 13 deletions packages/svelte-query/src/createQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import type {
} from '@tanstack/query-core'

import { notifyManager, QueriesObserver } from '@tanstack/query-core'
import { readable, type Readable } from 'svelte/store'
import { derived, get, readable, writable, type Readable } from 'svelte/store'

import type { CreateQueryOptions } from './types'
import type { CreateQueryOptions, WritableOrVal } from './types'
import { useQueryClient } from './useQueryClient'
import { isWritable } from './utils'

// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// `placeholderData` function does not have a parameter
Expand Down Expand Up @@ -155,34 +156,33 @@ export function createQueries<T extends any[]>({
queries,
queryClient,
}: {
queries: readonly [...QueriesOptions<T>]
queries: WritableOrVal<[...QueriesOptions<T>]>
queryClient?: QueryClient
}): CreateQueriesResult<T> {
const client = useQueryClient(queryClient)
// const isRestoring = useIsRestoring()

function getDefaultQuery(newQueries: readonly [...QueriesOptions<T>]) {
return newQueries.map((options) => {
const queriesStore = isWritable(queries) ? queries : writable(queries)

const defaultedQueriesStore = derived(queriesStore, ($queries) => {
return $queries.map((options) => {
const defaultedOptions = client.defaultQueryOptions(options)
// Make sure the results are already in fetching state before subscribing or updating options
defaultedOptions._optimisticResults = 'optimistic'

return defaultedOptions
})
}

const defaultedQueries = getDefaultQuery(queries)
let observer = new QueriesObserver(client, defaultedQueries)
})
const observer = new QueriesObserver(client, get(defaultedQueriesStore))

readable(observer).subscribe(($observer) => {
observer = $observer
defaultedQueriesStore.subscribe(($defaultedQueries) => {
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
observer.setQueries(defaultedQueries, { listeners: false })
observer.setQueries($defaultedQueries, { listeners: false })
})

const { subscribe } = readable(
observer.getOptimisticResult(defaultedQueries) as any,
observer.getOptimisticResult(get(defaultedQueriesStore)) as any,
(set) => {
return observer.subscribe(notifyManager.batchCalls(set))
},
Expand Down
Loading