Skip to content

Commit 0a0f0ff

Browse files
feat: Accept all svelte stores as options (#5672)
* refactor: Accept stores other than writable * Cleanup Readable type import * Add tests * Stricter isSvelteStore check * Update docs * Fix input bind * Add testing-library cleanup
1 parent e35941d commit 0a0f0ff

File tree

10 files changed

+84
-37
lines changed

10 files changed

+84
-37
lines changed

docs/svelte/reactivity.md

+5-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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 be able to react to changes in options you need to use [stores](https://svelte.dev/tutorial/writable-stores).
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/docs/svelte-store).
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

@@ -30,20 +30,18 @@ To solve this, create a store for the options and use it as input for the query.
3030
```markdown
3131
<script>
3232
import { createQuery } from '@tanstack/svelte-query'
33+
import type { CreateQueryOptions } from '@tanstack/svelte-query'
3334

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

3637
const queryOptions = writable({
3738
queryKey: ['refetch'],
3839
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
3940
refetchInterval: 1000,
40-
})
41-
const query = createQuery(queryOptions)
41+
}) satisfies CreateQueryOptions
4242

43-
function updateRefetchInterval(event) {
44-
$queryOptions.refetchInterval = event.target.valueAsNumber
45-
}
43+
const query = createQuery(queryOptions)
4644
</script>
4745

48-
<input type="number" on:input={updateRefetchInterval} />
46+
<input type="number" bind:value={$queryOptions.refetchInterval} />
4947
```

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

+49-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { describe, it, expect } from 'vitest'
1+
import { describe, expect, test } from 'vitest'
22
import { render, waitFor } from '@testing-library/svelte'
3-
import { writable } from 'svelte/store'
3+
import { derived, writable } from 'svelte/store'
44
import CreateQuery from './CreateQuery.svelte'
55
import { sleep } from './utils'
66
import type { CreateQueryOptions } from '../types'
77

88
describe('createQuery', () => {
9-
it('Render and wait for success', async () => {
9+
test('Render and wait for success', async () => {
1010
const rendered = render(CreateQuery, {
1111
props: {
1212
options: {
@@ -28,8 +28,8 @@ describe('createQuery', () => {
2828
})
2929
})
3030

31-
it('should keep previous data when returned as placeholder data', async () => {
32-
const options: CreateQueryOptions = writable({
31+
test('Keep previous data when returned as placeholder data', async () => {
32+
const options = writable({
3333
queryKey: ['test', [1]],
3434
queryFn: async ({ queryKey }) => {
3535
await sleep(10)
@@ -38,7 +38,8 @@ describe('createQuery', () => {
3838
return ids.map((id) => ({ id }))
3939
},
4040
placeholderData: (previousData: { id: number }[]) => previousData,
41-
})
41+
}) satisfies CreateQueryOptions
42+
4243
const rendered = render(CreateQuery, { props: { options } })
4344

4445
await waitFor(() => {
@@ -63,4 +64,46 @@ describe('createQuery', () => {
6364
expect(rendered.queryByText('id: 2')).toBeInTheDocument()
6465
})
6566
})
67+
68+
test('Accept a writable store for options', async () => {
69+
const optionsStore = writable({
70+
queryKey: ['test'],
71+
queryFn: async () => {
72+
await sleep(10)
73+
return 'Success'
74+
},
75+
}) satisfies CreateQueryOptions
76+
77+
const rendered = render(CreateQuery, {
78+
props: {
79+
options: optionsStore,
80+
},
81+
})
82+
83+
await waitFor(() => {
84+
expect(rendered.getByText('Success')).toBeInTheDocument()
85+
})
86+
})
87+
88+
test('Accept a derived store for options', async () => {
89+
const writableStore = writable('test')
90+
91+
const derivedStore = derived(writableStore, ($store) => ({
92+
queryKey: [$store],
93+
queryFn: async () => {
94+
await sleep(10)
95+
return 'Success'
96+
},
97+
})) satisfies CreateQueryOptions
98+
99+
const rendered = render(CreateQuery, {
100+
props: {
101+
options: derivedStore,
102+
},
103+
})
104+
105+
await waitFor(() => {
106+
expect(rendered.getByText('Success')).toBeInTheDocument()
107+
})
108+
})
66109
})

packages/svelte-query/src/createBaseQuery.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { derived, get, readable, writable } from 'svelte/store'
1+
import { derived, get, readable } from 'svelte/store'
22
import { notifyManager } from '@tanstack/query-core'
33
import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
44
import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types'
55
import { useQueryClient } from './useQueryClient'
6-
import { isWritable } from './utils'
6+
import { isSvelteStore } from './utils'
77

88
export function createBaseQuery<
99
TQueryFnData,
@@ -24,7 +24,7 @@ export function createBaseQuery<
2424
): CreateBaseQueryResult<TData, TError> {
2525
const client = useQueryClient(queryClient)
2626

27-
const optionsStore = isWritable(options) ? options : writable(options)
27+
const optionsStore = isSvelteStore(options) ? options : readable(options)
2828

2929
const defaultedOptionsStore = derived(optionsStore, ($options) => {
3030
const defaultedOptions = client.defaultQueryOptions($options)

packages/svelte-query/src/createMutation.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readable, derived, writable, get } from 'svelte/store'
1+
import { readable, derived, get } from 'svelte/store'
22
import type { QueryClient, DefaultError } from '@tanstack/query-core'
33
import { MutationObserver, notifyManager } from '@tanstack/query-core'
44
import type {
@@ -7,7 +7,7 @@ import type {
77
CreateMutationResult,
88
} from './types'
99
import { useQueryClient } from './useQueryClient'
10-
import { isWritable } from './utils'
10+
import { isSvelteStore } from './utils'
1111

1212
export function createMutation<
1313
TData = unknown,
@@ -20,7 +20,7 @@ export function createMutation<
2020
): CreateMutationResult<TData, TError, TVariables, TContext> {
2121
const client = useQueryClient(queryClient)
2222

23-
const optionsStore = isWritable(options) ? options : writable(options)
23+
const optionsStore = isSvelteStore(options) ? options : readable(options)
2424

2525
const observer = new MutationObserver<TData, TError, TVariables, TContext>(
2626
client,

packages/svelte-query/src/createQueries.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import type {
99
QueryObserverOptions,
1010
} from '@tanstack/query-core'
1111
import { notifyManager, QueriesObserver } from '@tanstack/query-core'
12-
import { derived, get, readable, writable, type Readable } from 'svelte/store'
13-
import type { WritableOrVal } from './types'
12+
import { derived, get, readable } from 'svelte/store'
13+
import type { Readable } from 'svelte/store'
14+
import type { StoreOrVal } from './types'
1415
import { useQueryClient } from './useQueryClient'
15-
import { isWritable } from './utils'
16+
import { isSvelteStore } from './utils'
1617

1718
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
1819
// `placeholderData` function does not have a parameter
@@ -168,15 +169,15 @@ export function createQueries<
168169
queries,
169170
...options
170171
}: {
171-
queries: WritableOrVal<[...QueriesOptions<T>]>
172+
queries: StoreOrVal<[...QueriesOptions<T>]>
172173
combine?: (result: QueriesResults<T>) => TCombinedResult
173174
},
174175
queryClient?: QueryClient,
175176
): Readable<TCombinedResult> {
176177
const client = useQueryClient(queryClient)
177178
// const isRestoring = useIsRestoring()
178179

179-
const queriesStore = isWritable(queries) ? queries : writable(queries)
180+
const queriesStore = isSvelteStore(queries) ? queries : readable(queries)
180181

181182
const defaultedQueriesStore = derived(queriesStore, ($queries) => {
182183
return $queries.map((opts) => {

packages/svelte-query/src/types.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
import type { Readable, Writable } from 'svelte/store'
1414

1515
/** Allows a type to be either the base object or a store of that object */
16-
export type WritableOrVal<T> = T | Writable<T>
16+
export type StoreOrVal<T> = T | Readable<T> | Writable<T>
1717

1818
/** Options for createBaseQuery */
1919
export type CreateBaseQueryOptions<
@@ -22,7 +22,7 @@ export type CreateBaseQueryOptions<
2222
TData = TQueryFnData,
2323
TQueryData = TQueryFnData,
2424
TQueryKey extends QueryKey = QueryKey,
25-
> = WritableOrVal<
25+
> = StoreOrVal<
2626
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
2727
>
2828

@@ -54,7 +54,7 @@ export type CreateInfiniteQueryOptions<
5454
TQueryData = TQueryFnData,
5555
TQueryKey extends QueryKey = QueryKey,
5656
TPageParam = unknown,
57-
> = WritableOrVal<
57+
> = StoreOrVal<
5858
InfiniteQueryObserverOptions<
5959
TQueryFnData,
6060
TError,
@@ -89,7 +89,7 @@ export type CreateMutationOptions<
8989
TError = DefaultError,
9090
TVariables = void,
9191
TContext = unknown,
92-
> = WritableOrVal<
92+
> = StoreOrVal<
9393
Omit<
9494
MutationObserverOptions<TData, TError, TVariables, TContext>,
9595
'_defaulted' | 'variables'

packages/svelte-query/src/useIsFetching.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
type QueryClient,
44
notifyManager,
55
} from '@tanstack/query-core'
6-
import { type Readable, readable } from 'svelte/store'
6+
import { readable } from 'svelte/store'
7+
import type { Readable } from 'svelte/store'
78
import { useQueryClient } from './useQueryClient'
89

910
export function useIsFetching(

packages/svelte-query/src/useIsMutating.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
type QueryClient,
44
notifyManager,
55
} from '@tanstack/query-core'
6-
import { type Readable, readable } from 'svelte/store'
6+
import { readable } from 'svelte/store'
7+
import type { Readable } from 'svelte/store'
78
import { useQueryClient } from './useQueryClient'
89

910
export function useIsMutating(

packages/svelte-query/src/utils.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { Writable } from 'svelte/store'
2-
import type { WritableOrVal } from './types'
1+
import type { Readable } from 'svelte/store'
2+
import type { StoreOrVal } from './types'
33

4-
export function isWritable<T extends object>(
5-
obj: WritableOrVal<T>,
6-
): obj is Writable<T> {
7-
return 'subscribe' in obj && 'set' in obj && 'update' in obj
4+
export function isSvelteStore<T extends object>(
5+
obj: StoreOrVal<T>,
6+
): obj is Readable<T> {
7+
return 'subscribe' in obj && typeof obj.subscribe === 'function'
88
}

packages/svelte-query/vitest.setup.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import matchers from '@testing-library/jest-dom/matchers'
2-
import { expect } from 'vitest'
2+
import { cleanup } from '@testing-library/svelte'
3+
import { afterEach, expect } from 'vitest'
34

45
expect.extend(matchers)
6+
7+
afterEach(() => cleanup())

0 commit comments

Comments
 (0)