Skip to content

Commit 254cd74

Browse files
chrislcsTkDodolachlancollins
authored
feat(svelte-query): use store for reactivity in options (#5050)
Co-authored-by: Dominik Dorfmeister <[email protected]> Co-authored-by: Lachlan Collins <[email protected]>
1 parent f7f1611 commit 254cd74

13 files changed

+164
-81
lines changed

docs/svelte/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ Svelte Query offers useful functions and components that will make managing serv
7171
Svelte Query offers an API similar to React Query, but there are some key differences to be mindful of.
7272

7373
- 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).
74-
- If your query or mutation depends on variables, you must assign it reactively. You can read more about this [here](./reactivity).
74+
- If your query or mutation depends on variables, you must use a store for the options. You can read more about this [here](./reactivity).

docs/svelte/reactivity.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ id: reactivity
33
title: Reactivity
44
---
55

6-
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.
6+
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).
77

88
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.
99

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

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

28-
To solve this, you can prefix the query with `$: ` to tell the compiler it should be reactive.
28+
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.
2929

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

34-
let intervalMs = 1000
35-
3634
const endpoint = 'http://localhost:5173/api/data'
3735

38-
$: query = createQuery({
36+
const queryOptions = writable({
3937
queryKey: ['refetch'],
4038
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
41-
refetchInterval: intervalMs,
39+
refetchInterval: 1000,
4240
})
41+
const query = createQuery(queryOptions)
42+
43+
function updateRefetchInterval(event) {
44+
$queryOptions.refetchInterval = event.target.valueAsNumber
45+
}
4346
</script>
4447

45-
<input bind:value={intervalMs} type="number" />
48+
<input type="number" on:input={updateRefetchInterval} />
4649
```

packages/svelte-query/src/__tests__/CreateQueries.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { setQueryClientContext } from '../context'
44
import type { QueriesOptions } from '../createQueries'
55
6-
export let options: { queries: readonly [...QueriesOptions<any>] }
6+
export let options: { queries: [...QueriesOptions<any>] }
77
88
const queryClient = new QueryClient()
99
setQueryClientContext(queryClient)

packages/svelte-query/src/__tests__/CreateQuery.svelte

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<script lang="ts">
2-
import { createQuery, QueryClient, type CreateQueryOptions } from '../index'
2+
import {
3+
createQuery,
4+
QueryClient,
5+
type CreateQueryOptions,
6+
type WritableOrVal,
7+
} from '../index'
38
import { setQueryClientContext } from '../context'
49
5-
export let options: CreateQueryOptions<any>
10+
export let options: WritableOrVal<CreateQueryOptions<any>>
611
712
const queryClient = new QueryClient()
813
setQueryClientContext(queryClient)
@@ -17,3 +22,9 @@
1722
{:else if $query.isSuccess}
1823
<p>Success</p>
1924
{/if}
25+
26+
<ul>
27+
{#each $query.data ?? [] as entry}
28+
<li>id: {entry.id}</li>
29+
{/each}
30+
</ul>

packages/svelte-query/src/__tests__/createQuery.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { describe, it, expect } from 'vitest'
22
import { render, waitFor } from '@testing-library/svelte'
3+
import { writable } from 'svelte/store'
34
import CreateQuery from './CreateQuery.svelte'
45
import { sleep } from './utils'
6+
import type { CreateQueryOptions, WritableOrVal } from '../types'
57

68
describe('createQuery', () => {
79
it('Render and wait for success', async () => {
@@ -25,4 +27,38 @@ describe('createQuery', () => {
2527
expect(rendered.getByText('Success')).toBeInTheDocument()
2628
})
2729
})
30+
31+
it('should keep previous data when returned as placeholder data', async () => {
32+
const options: WritableOrVal<CreateQueryOptions> = writable({
33+
queryKey: ['test', [1]],
34+
queryFn: async ({ queryKey }) => {
35+
await sleep(50)
36+
const ids = queryKey[1]
37+
if (!ids || !Array.isArray(ids)) return []
38+
return ids.map((id) => ({ id }))
39+
},
40+
placeholderData: (previousData: { id: number }[]) => previousData,
41+
})
42+
const rendered = render(CreateQuery, { props: { options } })
43+
44+
expect(rendered.queryByText('id: 1')).not.toBeInTheDocument()
45+
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
46+
47+
await sleep(100)
48+
49+
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
50+
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
51+
52+
options.update((o) => ({ ...o, queryKey: ['test', [1, 2]] }))
53+
54+
await sleep(0)
55+
56+
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
57+
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
58+
59+
await sleep(100)
60+
61+
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
62+
expect(rendered.queryByText('id: 2')).toBeInTheDocument()
63+
})
2864
})

packages/svelte-query/src/createBaseQuery.ts

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
22
import { notifyManager } from '@tanstack/query-core'
3-
import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types'
3+
import type {
4+
CreateBaseQueryOptions,
5+
CreateBaseQueryResult,
6+
WritableOrVal,
7+
} from './types'
48
import { useQueryClient } from './useQueryClient'
5-
import { derived, readable } from 'svelte/store'
9+
import { derived, get, readable, writable } from 'svelte/store'
10+
import { isWritable } from './utils'
611

712
export function createBaseQuery<
813
TQueryFnData,
@@ -11,61 +16,63 @@ export function createBaseQuery<
1116
TQueryData,
1217
TQueryKey extends QueryKey,
1318
>(
14-
options: CreateBaseQueryOptions<
15-
TQueryFnData,
16-
TError,
17-
TData,
18-
TQueryData,
19-
TQueryKey
19+
options: WritableOrVal<
20+
CreateBaseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
2021
>,
2122
Observer: typeof QueryObserver,
2223
queryClient?: QueryClient,
2324
): CreateBaseQueryResult<TData, TError> {
2425
const client = useQueryClient(queryClient)
25-
const defaultedOptions = client.defaultQueryOptions(options)
26-
defaultedOptions._optimisticResults = 'optimistic'
2726

28-
let observer = new Observer<
27+
const optionsStore = isWritable(options) ? options : writable(options)
28+
29+
const defaultedOptionsStore = derived(optionsStore, ($options) => {
30+
const defaultedOptions = client.defaultQueryOptions($options)
31+
defaultedOptions._optimisticResults = 'optimistic'
32+
33+
// Include callbacks in batch renders
34+
if (defaultedOptions.onError) {
35+
defaultedOptions.onError = notifyManager.batchCalls(
36+
defaultedOptions.onError,
37+
)
38+
}
39+
40+
if (defaultedOptions.onSuccess) {
41+
defaultedOptions.onSuccess = notifyManager.batchCalls(
42+
defaultedOptions.onSuccess,
43+
)
44+
}
45+
46+
if (defaultedOptions.onSettled) {
47+
defaultedOptions.onSettled = notifyManager.batchCalls(
48+
defaultedOptions.onSettled,
49+
)
50+
}
51+
52+
return defaultedOptions
53+
})
54+
55+
const observer = new Observer<
2956
TQueryFnData,
3057
TError,
3158
TData,
3259
TQueryData,
3360
TQueryKey
34-
>(client, defaultedOptions)
35-
36-
// Include callbacks in batch renders
37-
if (defaultedOptions.onError) {
38-
defaultedOptions.onError = notifyManager.batchCalls(
39-
defaultedOptions.onError,
40-
)
41-
}
42-
43-
if (defaultedOptions.onSuccess) {
44-
defaultedOptions.onSuccess = notifyManager.batchCalls(
45-
defaultedOptions.onSuccess,
46-
)
47-
}
48-
49-
if (defaultedOptions.onSettled) {
50-
defaultedOptions.onSettled = notifyManager.batchCalls(
51-
defaultedOptions.onSettled,
52-
)
53-
}
61+
>(client, get(defaultedOptionsStore))
5462

55-
readable(observer).subscribe(($observer) => {
56-
observer = $observer
63+
defaultedOptionsStore.subscribe(($defaultedOptions) => {
5764
// Do not notify on updates because of changes in the options because
5865
// these changes should already be reflected in the optimistic result.
59-
observer.setOptions(defaultedOptions, { listeners: false })
66+
observer.setOptions($defaultedOptions, { listeners: false })
6067
})
6168

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

6673
const { subscribe } = derived(result, ($result) => {
67-
$result = observer.getOptimisticResult(defaultedOptions)
68-
return !defaultedOptions.notifyOnChangeProps
74+
$result = observer.getOptimisticResult(get(defaultedOptionsStore))
75+
return !get(defaultedOptionsStore).notifyOnChangeProps
6976
? observer.trackResult($result)
7077
: $result
7178
})

packages/svelte-query/src/createInfiniteQuery.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { InfiniteQueryObserver } from '@tanstack/query-core'
99
import type {
1010
CreateInfiniteQueryOptions,
1111
CreateInfiniteQueryResult,
12+
WritableOrVal,
1213
} from './types'
1314
import { createBaseQuery } from './createBaseQuery'
1415

@@ -19,13 +20,15 @@ export function createInfiniteQuery<
1920
TQueryKey extends QueryKey = QueryKey,
2021
TPageParam = unknown,
2122
>(
22-
options: CreateInfiniteQueryOptions<
23-
TQueryFnData,
24-
TError,
25-
TData,
26-
TQueryFnData,
27-
TQueryKey,
28-
TPageParam
23+
options: WritableOrVal<
24+
CreateInfiniteQueryOptions<
25+
TQueryFnData,
26+
TError,
27+
TData,
28+
TQueryFnData,
29+
TQueryKey,
30+
TPageParam
31+
>
2932
>,
3033
queryClient?: QueryClient,
3134
): CreateInfiniteQueryResult<TData, TError> {

packages/svelte-query/src/createMutation.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
1-
import { readable, derived } from 'svelte/store'
1+
import { readable, derived, writable, get } from 'svelte/store'
22
import type { QueryClient, RegisteredError } from '@tanstack/query-core'
33
import { MutationObserver, notifyManager } from '@tanstack/query-core'
44
import type {
55
CreateMutateFunction,
66
CreateMutationOptions,
77
CreateMutationResult,
8+
WritableOrVal,
89
} from './types'
910
import { useQueryClient } from './useQueryClient'
11+
import { isWritable } from './utils'
1012

1113
export function createMutation<
1214
TData = unknown,
1315
TError = RegisteredError,
1416
TVariables = void,
1517
TContext = unknown,
1618
>(
17-
options: CreateMutationOptions<TData, TError, TVariables, TContext>,
19+
options: WritableOrVal<
20+
CreateMutationOptions<TData, TError, TVariables, TContext>
21+
>,
1822
queryClient?: QueryClient,
1923
): CreateMutationResult<TData, TError, TVariables, TContext> {
2024
const client = useQueryClient(queryClient)
21-
let observer = new MutationObserver<TData, TError, TVariables, TContext>(
25+
26+
const optionsStore = isWritable(options) ? options : writable(options)
27+
28+
const observer = new MutationObserver<TData, TError, TVariables, TContext>(
2229
client,
23-
options,
30+
get(optionsStore),
2431
)
2532
let mutate: CreateMutateFunction<TData, TError, TVariables, TContext>
2633

27-
readable(observer).subscribe(($observer) => {
28-
observer = $observer
34+
optionsStore.subscribe(($options) => {
2935
mutate = (variables, mutateOptions) => {
3036
observer.mutate(variables, mutateOptions).catch(noop)
3137
}
32-
observer.setOptions(options)
38+
observer.setOptions($options)
3339
})
3440

3541
const result = readable(observer.getCurrentResult(), (set) => {

packages/svelte-query/src/createQueries.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import type {
88
} from '@tanstack/query-core'
99

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

13-
import type { CreateQueryOptions } from './types'
13+
import type { CreateQueryOptions, WritableOrVal } from './types'
1414
import { useQueryClient } from './useQueryClient'
15+
import { isWritable } from './utils'
1516

1617
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
1718
// `placeholderData` function does not have a parameter
@@ -155,34 +156,33 @@ export function createQueries<T extends any[]>({
155156
queries,
156157
queryClient,
157158
}: {
158-
queries: readonly [...QueriesOptions<T>]
159+
queries: WritableOrVal<[...QueriesOptions<T>]>
159160
queryClient?: QueryClient
160161
}): CreateQueriesResult<T> {
161162
const client = useQueryClient(queryClient)
162163
// const isRestoring = useIsRestoring()
163164

164-
function getDefaultQuery(newQueries: readonly [...QueriesOptions<T>]) {
165-
return newQueries.map((options) => {
165+
const queriesStore = isWritable(queries) ? queries : writable(queries)
166+
167+
const defaultedQueriesStore = derived(queriesStore, ($queries) => {
168+
return $queries.map((options) => {
166169
const defaultedOptions = client.defaultQueryOptions(options)
167170
// Make sure the results are already in fetching state before subscribing or updating options
168171
defaultedOptions._optimisticResults = 'optimistic'
169172

170173
return defaultedOptions
171174
})
172-
}
173-
174-
const defaultedQueries = getDefaultQuery(queries)
175-
let observer = new QueriesObserver(client, defaultedQueries)
175+
})
176+
const observer = new QueriesObserver(client, get(defaultedQueriesStore))
176177

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

184184
const { subscribe } = readable(
185-
observer.getOptimisticResult(defaultedQueries) as any,
185+
observer.getOptimisticResult(get(defaultedQueriesStore)) as any,
186186
(set) => {
187187
return observer.subscribe(notifyManager.batchCalls(set))
188188
},

0 commit comments

Comments
 (0)