Skip to content

Commit 0b2ddb5

Browse files
chore(svelte-query): Update reactivity docs and tests (#5683)
1 parent 43dc530 commit 0b2ddb5

File tree

3 files changed

+114
-67
lines changed

3 files changed

+114
-67
lines changed

docs/svelte/reactivity.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,47 @@ 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/docs/svelte-store).
6+
Svelte uses a compiler to build your code which optimises rendering. By default, components 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

8-
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.
8+
In the below example, the `refetchInterval` option is set from the variable `intervalMs`, which is bound to the input field. However, as the query is not able to react to changes in `intervalMs`, `refetchInterval` will not change when the input value changes.
99

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

14-
let intervalMs = 1000
15-
1614
const endpoint = 'http://localhost:5173/api/data'
1715

16+
let intervalMs = 1000
17+
1818
const query = createQuery({
1919
queryKey: ['refetch'],
2020
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
2121
refetchInterval: intervalMs,
2222
})
2323
</script>
2424

25-
<input bind:value={intervalMs} type="number" />
25+
<input type="number" bind:value={intervalMs} />
2626
```
2727

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.
28+
To solve this, we can convert `intervalMs` into a writable store. The query options can then be turned into a derived store, which will be passed into the function with true reactivity.
2929

3030
```markdown
31-
<script>
31+
<script lang="ts">
32+
import { derived, writable } from 'svelte/store'
3233
import { createQuery } from '@tanstack/svelte-query'
33-
import type { CreateQueryOptions } from '@tanstack/svelte-query'
3434

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

37-
const queryOptions = writable({
38-
queryKey: ['refetch'],
39-
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
40-
refetchInterval: 1000,
41-
}) satisfies CreateQueryOptions
37+
const intervalMs = writable(1000)
4238

43-
const query = createQuery(queryOptions)
39+
const query = createQuery(
40+
derived(intervalMs, ($intervalMs) => ({
41+
queryKey: ['refetch'],
42+
queryFn: async () => await fetch(endpoint).then((r) => r.json()),
43+
refetchInterval: $intervalMs,
44+
}))
45+
)
4446
</script>
4547

46-
<input type="number" bind:value={$queryOptions.refetchInterval} />
48+
<input type="number" bind:value={$intervalMs} />
4749
```
Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
<script lang="ts">
2-
import { QueryClient } from '@tanstack/query-core'
3-
import { setQueryClientContext } from '../context'
42
import { createQuery } from '../createQuery'
3+
import type { QueryClient } from '@tanstack/query-core'
54
import type { CreateQueryOptions } from '../types'
65
76
export let options: CreateQueryOptions<any>
7+
export let queryClient: QueryClient
88
9-
const queryClient = new QueryClient()
10-
setQueryClientContext(queryClient)
11-
12-
const query = createQuery(options)
9+
const query = createQuery(options, queryClient)
1310
</script>
1411

1512
{#if $query.isPending}
1613
<p>Loading</p>
1714
{:else if $query.isError}
1815
<p>Error</p>
1916
{:else if $query.isSuccess}
20-
<p>Success</p>
17+
{#if Array.isArray($query.data)}
18+
{#each $query.data as item}
19+
<p>{item}</p>
20+
{/each}
21+
{:else}
22+
<p>{$query.data}</p>
23+
{/if}
2124
{/if}
22-
23-
<ul>
24-
{#each $query.data ?? [] as entry}
25-
<li>id: {entry.id}</li>
26-
{/each}
27-
</ul>
Lines changed: 86 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { describe, expect, test } from 'vitest'
22
import { render, waitFor } from '@testing-library/svelte'
33
import { derived, writable } from 'svelte/store'
4+
import { QueryClient } from '@tanstack/query-core'
45
import CreateQuery from './CreateQuery.svelte'
56
import { sleep } from './utils'
6-
import type { CreateQueryOptions } from '../types'
77

88
describe('createQuery', () => {
99
test('Render and wait for success', async () => {
@@ -16,94 +16,142 @@ describe('createQuery', () => {
1616
return 'Success'
1717
},
1818
},
19+
queryClient: new QueryClient(),
1920
},
2021
})
2122

2223
await waitFor(() => {
23-
expect(rendered.getByText('Loading')).toBeInTheDocument()
24+
expect(rendered.queryByText('Loading')).toBeInTheDocument()
2425
})
2526

2627
await waitFor(() => {
27-
expect(rendered.getByText('Success')).toBeInTheDocument()
28+
expect(rendered.queryByText('Success')).toBeInTheDocument()
2829
})
2930
})
3031

31-
test('Keep previous data when returned as placeholder data', async () => {
32-
const options = writable({
33-
queryKey: ['test', [1]],
34-
queryFn: async ({ queryKey }) => {
32+
test('Accept a writable store for options', async () => {
33+
const optionsStore = writable({
34+
queryKey: ['test'],
35+
queryFn: async () => {
3536
await sleep(10)
36-
const ids = queryKey[1]
37-
if (!ids || !Array.isArray(ids)) return []
38-
return ids.map((id) => ({ id }))
37+
return 'Success'
3938
},
40-
placeholderData: (previousData: { id: number }[]) => previousData,
41-
}) satisfies CreateQueryOptions
42-
43-
const rendered = render(CreateQuery, { props: { options } })
39+
})
4440

45-
await waitFor(() => {
46-
expect(rendered.queryByText('id: 1')).not.toBeInTheDocument()
47-
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
41+
const rendered = render(CreateQuery, {
42+
props: {
43+
options: optionsStore,
44+
queryClient: new QueryClient(),
45+
},
4846
})
4947

5048
await waitFor(() => {
51-
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
52-
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
49+
expect(rendered.queryByText('Success')).toBeInTheDocument()
5350
})
51+
})
52+
53+
test('Accept a derived store for options', async () => {
54+
const writableStore = writable('test')
5455

55-
options.update((o) => ({ ...o, queryKey: ['test', [1, 2]] }))
56+
const derivedStore = derived(writableStore, ($store) => ({
57+
queryKey: [$store],
58+
queryFn: async () => {
59+
await sleep(10)
60+
return 'Success'
61+
},
62+
}))
5663

57-
await waitFor(() => {
58-
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
59-
expect(rendered.queryByText('id: 2')).not.toBeInTheDocument()
64+
const rendered = render(CreateQuery, {
65+
props: {
66+
options: derivedStore,
67+
queryClient: new QueryClient(),
68+
},
6069
})
6170

6271
await waitFor(() => {
63-
expect(rendered.queryByText('id: 1')).toBeInTheDocument()
64-
expect(rendered.queryByText('id: 2')).toBeInTheDocument()
72+
expect(rendered.queryByText('Success')).toBeInTheDocument()
6573
})
6674
})
6775

68-
test('Accept a writable store for options', async () => {
69-
const optionsStore = writable({
70-
queryKey: ['test'],
76+
test('Ensure reactivity when queryClient defaults are set', async () => {
77+
const writableStore = writable(1)
78+
79+
const derivedStore = derived(writableStore, ($store) => ({
80+
queryKey: [$store],
7181
queryFn: async () => {
7282
await sleep(10)
73-
return 'Success'
83+
return `Success ${$store}`
7484
},
75-
}) satisfies CreateQueryOptions
85+
}))
7686

7787
const rendered = render(CreateQuery, {
7888
props: {
79-
options: optionsStore,
89+
options: derivedStore,
90+
queryClient: new QueryClient({
91+
defaultOptions: { queries: { staleTime: 60 * 1000 } },
92+
}),
8093
},
8194
})
8295

8396
await waitFor(() => {
84-
expect(rendered.getByText('Success')).toBeInTheDocument()
97+
expect(rendered.queryByText('Success 1')).toBeInTheDocument()
98+
expect(rendered.queryByText('Success 2')).not.toBeInTheDocument()
99+
})
100+
101+
writableStore.set(2)
102+
103+
await waitFor(() => {
104+
expect(rendered.queryByText('Success 1')).not.toBeInTheDocument()
105+
expect(rendered.queryByText('Success 2')).toBeInTheDocument()
106+
})
107+
108+
writableStore.set(1)
109+
110+
await waitFor(() => {
111+
expect(rendered.queryByText('Success 1')).toBeInTheDocument()
112+
expect(rendered.queryByText('Success 2')).not.toBeInTheDocument()
85113
})
86114
})
87115

88-
test('Accept a derived store for options', async () => {
89-
const writableStore = writable('test')
116+
test('Keep previous data when returned as placeholder data', async () => {
117+
const writableStore = writable<number[]>([1])
90118

91119
const derivedStore = derived(writableStore, ($store) => ({
92-
queryKey: [$store],
120+
queryKey: ['test', $store],
93121
queryFn: async () => {
94122
await sleep(10)
95-
return 'Success'
123+
return $store.map((id) => `Success ${id}`)
96124
},
97-
})) satisfies CreateQueryOptions
125+
placeholderData: (previousData: string) => previousData,
126+
}))
98127

99128
const rendered = render(CreateQuery, {
100129
props: {
101130
options: derivedStore,
131+
queryClient: new QueryClient(),
102132
},
103133
})
104134

105135
await waitFor(() => {
106-
expect(rendered.getByText('Success')).toBeInTheDocument()
136+
expect(rendered.queryByText('Success 1')).not.toBeInTheDocument()
137+
expect(rendered.queryByText('Success 2')).not.toBeInTheDocument()
138+
})
139+
140+
await waitFor(() => {
141+
expect(rendered.queryByText('Success 1')).toBeInTheDocument()
142+
expect(rendered.queryByText('Success 2')).not.toBeInTheDocument()
143+
})
144+
145+
writableStore.set([1, 2])
146+
147+
await waitFor(() => {
148+
expect(rendered.queryByText('Success 1')).toBeInTheDocument()
149+
expect(rendered.queryByText('Success 2')).not.toBeInTheDocument()
150+
})
151+
152+
await waitFor(() => {
153+
expect(rendered.queryByText('Success 1')).toBeInTheDocument()
154+
expect(rendered.queryByText('Success 2')).toBeInTheDocument()
107155
})
108156
})
109157
})

0 commit comments

Comments
 (0)