diff --git a/docs/config.json b/docs/config.json
index e09f9d1de9..5a3dadd369 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -127,10 +127,6 @@
{
"label": "SSR & SvelteKit",
"to": "framework/svelte/ssr"
- },
- {
- "label": "Reactivity",
- "to": "framework/svelte/reactivity"
}
]
},
diff --git a/docs/framework/svelte/devtools.md b/docs/framework/svelte/devtools.md
index db495f2c0e..c5dbfd4bef 100644
--- a/docs/framework/svelte/devtools.md
+++ b/docs/framework/svelte/devtools.md
@@ -55,7 +55,7 @@ Place the following code as high in your Svelte app as you can. The closer it is
### Options
-- `initialIsOpen: Boolean`
+- `initialIsOpen: boolean`
- Set this `true` if you want the dev tools to default to being open
- `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative"`
- Defaults to `bottom-right`
diff --git a/docs/framework/svelte/installation.md b/docs/framework/svelte/installation.md
index e4ad607ee0..7fd45eb7df 100644
--- a/docs/framework/svelte/installation.md
+++ b/docs/framework/svelte/installation.md
@@ -5,8 +5,6 @@ title: Installation
You can install Svelte Query via [NPM](https://npmjs.com).
-> v5 is currently available as a release-candidate. We don't anticipate any major API changes from here on out. We encourage you to try it out and report any issues you find.
-
### NPM
```bash
diff --git a/docs/framework/svelte/overview.md b/docs/framework/svelte/overview.md
index f1122355aa..e2cc39531e 100644
--- a/docs/framework/svelte/overview.md
+++ b/docs/framework/svelte/overview.md
@@ -28,19 +28,19 @@ Then call any function (e.g. createQuery) from any component:
- {#if $query.isLoading}
+ {#if query.isLoading}
Loading...
- {:else if $query.isError}
-
Error: {$query.error.message}
- {:else if $query.isSuccess}
- {#each $query.data as todo}
+ {:else if query.isError}
+
Error: {query.error.message}
+ {:else if query.isSuccess}
+ {#each query.data as todo}
{todo.title}
{/each}
{/if}
@@ -62,6 +62,8 @@ Svelte Query offers useful functions and components that will make managing serv
- `useQueryClient`
- `useIsFetching`
- `useIsMutating`
+- `useMutationState`
+- `useIsRestoring`
- `useHydrate`
- `
`
- ``
@@ -70,5 +72,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://learn.svelte.dev/tutorial/writable-stores).
-- If your query or mutation depends on variables, you must use a store for the options. You can read more about this [here](../reactivity).
+- The arguments to the `create*` functions must be wrapped in a function to preserve reactivity.
diff --git a/docs/framework/svelte/reactivity.md b/docs/framework/svelte/reactivity.md
deleted file mode 100644
index 8fdab9d13a..0000000000
--- a/docs/framework/svelte/reactivity.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-id: reactivity
-title: Reactivity
----
-
-Svelte uses a compiler to build your code which optimizes 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).
-
-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.
-
-```svelte
-
-
-
-```
-
-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.
-
-```svelte
-
-
-
-```
diff --git a/docs/framework/svelte/ssr.md b/docs/framework/svelte/ssr.md
index ac6d5ee7ae..7448229caa 100644
--- a/docs/framework/svelte/ssr.md
+++ b/docs/framework/svelte/ssr.md
@@ -58,11 +58,11 @@ export async function load() {
export let data: PageData
- const query = createQuery({
+ const query = createQuery(() => ({
queryKey: ['posts'],
queryFn: getPosts,
initialData: data.posts,
- })
+ }))
```
@@ -136,10 +136,10 @@ export async function load({ parent, fetch }) {
import { createQuery } from '@tanstack/svelte-query'
// This data is cached by prefetchQuery in +page.ts so no fetch actually happens here
- const query = createQuery({
+ const query = createQuery(() => ({
queryKey: ['posts'],
queryFn: async () => (await fetch('/api/posts')).json(),
- })
+ }))
```
diff --git a/examples/svelte/auto-refetching/src/routes/+page.svelte b/examples/svelte/auto-refetching/src/routes/+page.svelte
index 40fdc0e541..4a31304000 100644
--- a/examples/svelte/auto-refetching/src/routes/+page.svelte
+++ b/examples/svelte/auto-refetching/src/routes/+page.svelte
@@ -10,7 +10,7 @@
const client = useQueryClient()
- const endpoint = 'http://localhost:5173/api/data'
+ const endpoint = '/api/data'
const todos = createQuery<{ items: string[] }>(() => ({
queryKey: ['refetch'],
@@ -21,7 +21,9 @@
const addMutation = createMutation(() => ({
mutationFn: (value: string) =>
- fetch(`${endpoint}?add=${value}`).then((r) => r.json()),
+ fetch(`${endpoint}?add=${encodeURIComponent(value)}`).then((r) =>
+ r.json(),
+ ),
onSuccess: () => client.invalidateQueries({ queryKey: ['refetch'] }),
}))
@@ -31,7 +33,7 @@
}))
-Auto Refetch with stale-time set to 1s
+Auto Refetch with stale-time set to {(intervalMs / 1000).toFixed(2)}s
This example is best experienced on your own machine, where you can open
@@ -86,14 +88,22 @@
clearMutation.mutate(undefined)}> Clear All
{/if}
-{#if todos.isFetching}
-
- 'Background Updating...' : ' '
-
-{/if}
+
+Background Updating...
diff --git a/examples/svelte/basic/src/lib/Posts.svelte b/examples/svelte/basic/src/lib/Posts.svelte
index e6a0851ee2..1f19e7fe32 100644
--- a/examples/svelte/basic/src/lib/Posts.svelte
+++ b/examples/svelte/basic/src/lib/Posts.svelte
@@ -38,11 +38,9 @@
{/each}
- {#if posts.isFetching}
-
- Background Updating...
-
- {/if}
+ Background Updating...
{/if}
@@ -53,8 +51,16 @@
}
a {
display: block;
- color: white;
font-size: 1.5rem;
margin-bottom: 1rem;
}
+
+ .updating-text {
+ color: transparent;
+ transition: all 0.3s ease;
+ }
+ .updating-text.on {
+ color: green;
+ transition: none;
+ }
diff --git a/examples/svelte/load-more-infinite-scroll/src/lib/LoadMore.svelte b/examples/svelte/load-more-infinite-scroll/src/lib/LoadMore.svelte
index 32f6e8971d..c03a65441a 100644
--- a/examples/svelte/load-more-infinite-scroll/src/lib/LoadMore.svelte
+++ b/examples/svelte/load-more-infinite-scroll/src/lib/LoadMore.svelte
@@ -60,5 +60,6 @@
.card {
background-color: #111;
margin-bottom: 1rem;
+ color: rgba(255, 255, 255, 0.87);
}
diff --git a/examples/svelte/optimistic-updates/src/routes/+page.svelte b/examples/svelte/optimistic-updates/src/routes/+page.svelte
index feb5d1085c..0caf5ffe7b 100644
--- a/examples/svelte/optimistic-updates/src/routes/+page.svelte
+++ b/examples/svelte/optimistic-updates/src/routes/+page.svelte
@@ -20,7 +20,7 @@
const client = useQueryClient()
- const endpoint = 'http://localhost:5173/api/data'
+ const endpoint = '/api/data'
const fetchTodos = async (): Promise =>
await fetch(endpoint).then((r) => r.json())
diff --git a/examples/svelte/optimistic-updates/src/routes/api/data/+server.ts b/examples/svelte/optimistic-updates/src/routes/api/data/+server.ts
index 46bfe05612..9cf65a54d2 100644
--- a/examples/svelte/optimistic-updates/src/routes/api/data/+server.ts
+++ b/examples/svelte/optimistic-updates/src/routes/api/data/+server.ts
@@ -6,7 +6,7 @@ type Todo = {
text: string
}
-const items: Todo[] = []
+const items: Array = []
/** @type {import('./$types').RequestHandler} */
export const GET: RequestHandler = async (req) => {
diff --git a/examples/svelte/playground/src/routes/AddTodo.svelte b/examples/svelte/playground/src/routes/AddTodo.svelte
index 514e4b8ee7..f482f6c2f1 100644
--- a/examples/svelte/playground/src/routes/AddTodo.svelte
+++ b/examples/svelte/playground/src/routes/AddTodo.svelte
@@ -14,7 +14,6 @@
let name = $state('')
const postTodo = async ({ name, notes }: Omit) => {
- console.info('postTodo', { name, notes })
return new Promise((resolve, reject) => {
setTimeout(
() => {
@@ -31,7 +30,7 @@
}
const todo = { name, notes, id: id.value }
id.value = id.value + 1
- list.value = [...list.value, todo]
+ list.value.push(todo)
resolve(todo)
},
queryTimeMin.value +
diff --git a/examples/svelte/playground/src/routes/App.svelte b/examples/svelte/playground/src/routes/App.svelte
index 04ddbb9b40..bd909aae90 100644
--- a/examples/svelte/playground/src/routes/App.svelte
+++ b/examples/svelte/playground/src/routes/App.svelte
@@ -26,7 +26,7 @@
{
- views.value = [...views.value, '']
+ views.value.push('')
}}
>
Add Filter List
diff --git a/examples/svelte/ssr/src/lib/Posts.svelte b/examples/svelte/ssr/src/lib/Posts.svelte
index 7f7065e813..5de76de86a 100644
--- a/examples/svelte/ssr/src/lib/Posts.svelte
+++ b/examples/svelte/ssr/src/lib/Posts.svelte
@@ -38,11 +38,9 @@
{/each}
- {#if posts.isFetching}
-
- Background Updating...
-
- {/if}
+ Background Updating...
{/if}
@@ -53,8 +51,15 @@
}
a {
display: block;
- color: white;
font-size: 1.5rem;
margin-bottom: 1rem;
}
+ .updating-text {
+ color: transparent;
+ transition: all 0.3s ease;
+ }
+ .updating-text.on {
+ color: green;
+ transition: none;
+ }
diff --git a/examples/svelte/ssr/src/routes/+layout.ts b/examples/svelte/ssr/src/routes/+layout.ts
index 5104825207..f922afcc92 100644
--- a/examples/svelte/ssr/src/routes/+layout.ts
+++ b/examples/svelte/ssr/src/routes/+layout.ts
@@ -1,6 +1,6 @@
-import { browser } from '$app/environment'
import { QueryClient } from '@tanstack/svelte-query'
import type { LayoutLoad } from './$types'
+import { browser } from '$app/environment'
export const load: LayoutLoad = () => {
const queryClient = new QueryClient({
diff --git a/examples/svelte/ssr/src/routes/+page.ts b/examples/svelte/ssr/src/routes/+page.ts
index 22d8f8ffbe..811b0d3a14 100644
--- a/examples/svelte/ssr/src/routes/+page.ts
+++ b/examples/svelte/ssr/src/routes/+page.ts
@@ -1,5 +1,5 @@
-import { api } from '$lib/api'
import type { PageLoad } from './$types'
+import { api } from '$lib/api'
export const load: PageLoad = async ({ parent, fetch }) => {
const { queryClient } = await parent()
diff --git a/examples/svelte/ssr/src/routes/[postId]/+page.ts b/examples/svelte/ssr/src/routes/[postId]/+page.ts
index b9cca0729b..87c9fa8a43 100644
--- a/examples/svelte/ssr/src/routes/[postId]/+page.ts
+++ b/examples/svelte/ssr/src/routes/[postId]/+page.ts
@@ -1,5 +1,5 @@
-import { api } from '$lib/api'
import type { PageLoad } from './$types'
+import { api } from '$lib/api'
export const load: PageLoad = async ({ parent, fetch, params }) => {
const { queryClient } = await parent()
diff --git a/examples/svelte/star-wars/src/routes/+page.svelte b/examples/svelte/star-wars/src/routes/+page.svelte
index eaaf33aa03..939c72ec97 100644
--- a/examples/svelte/star-wars/src/routes/+page.svelte
+++ b/examples/svelte/star-wars/src/routes/+page.svelte
@@ -2,8 +2,7 @@
React Query Demo
Using the Star Wars API
- (Built by @Brent_m_Clark
- )
+ (Built by @Brent_m_Clark )
Why React Query?
diff --git a/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.svelte b/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.svelte
index 3298c72c5a..03b77de532 100644
--- a/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.svelte
+++ b/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.svelte
@@ -2,18 +2,17 @@
import { createQuery } from '@tanstack/svelte-query'
import Homeworld from './Homeworld.svelte'
import Film from './Film.svelte'
-
- let { data } = $props()
+ import { page } from '$app/state'
const getCharacter = async () => {
const res = await fetch(
- `https://swapi.dev/api/people/${data.params.characterId}/`,
+ `https://swapi.dev/api/people/${page.params.characterId}/`,
)
return await res.json()
}
const query = createQuery(() => ({
- queryKey: ['character', data.params.characterId],
+ queryKey: ['character', page.params.characterId],
queryFn: getCharacter,
}))
diff --git a/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.ts b/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.ts
deleted file mode 100644
index dbfde8eb56..0000000000
--- a/examples/svelte/star-wars/src/routes/characters/[characterId]/+page.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { PageLoad } from './$types'
-
-export const load: PageLoad = ({ params }) => {
- return { params }
-}
diff --git a/examples/svelte/star-wars/src/routes/films/[filmId]/+page.svelte b/examples/svelte/star-wars/src/routes/films/[filmId]/+page.svelte
index 84f1abffe9..47d6b69376 100644
--- a/examples/svelte/star-wars/src/routes/films/[filmId]/+page.svelte
+++ b/examples/svelte/star-wars/src/routes/films/[filmId]/+page.svelte
@@ -1,18 +1,17 @@
diff --git a/examples/svelte/star-wars/src/routes/films/[filmId]/+page.ts b/examples/svelte/star-wars/src/routes/films/[filmId]/+page.ts
deleted file mode 100644
index dbfde8eb56..0000000000
--- a/examples/svelte/star-wars/src/routes/films/[filmId]/+page.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { PageLoad } from './$types'
-
-export const load: PageLoad = ({ params }) => {
- return { params }
-}
diff --git a/packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte b/packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte
index c2653232b3..d94c4fbe20 100644
--- a/packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte
+++ b/packages/svelte-query-persist-client/src/PersistQueryClientProvider.svelte
@@ -7,6 +7,7 @@
QueryClientProvider,
setIsRestoringContext,
} from '@tanstack/svelte-query'
+ import { box } from './utils.svelte.js'
import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core'
import type {
OmitKeyof,
@@ -26,9 +27,9 @@
...props
}: PersistQueryClientProviderProps = $props()
- let isRestoring = $state(true)
+ let isRestoring = box(true)
- setIsRestoringContext(() => isRestoring)
+ setIsRestoringContext(isRestoring)
const options = $derived({
...persistOptions,
@@ -36,16 +37,16 @@
})
$effect(() => {
- return isRestoring ? () => {} : persistQueryClientSubscribe(options)
+ return isRestoring.current ? () => {} : persistQueryClientSubscribe(options)
})
$effect(() => {
- isRestoring = true
+ isRestoring.current = true
persistQueryClientRestore(options)
.then(() => props.onSuccess?.())
.catch(() => props.onError?.())
.finally(() => {
- isRestoring = false
+ isRestoring.current = false
})
})
diff --git a/packages/svelte-query-persist-client/src/utils.svelte.ts b/packages/svelte-query-persist-client/src/utils.svelte.ts
new file mode 100644
index 0000000000..7760eded8c
--- /dev/null
+++ b/packages/svelte-query-persist-client/src/utils.svelte.ts
@@ -0,0 +1,14 @@
+type Box = { current: T }
+
+export function box(initial: T): Box {
+ let current = $state(initial)
+
+ return {
+ get current() {
+ return current
+ },
+ set current(newValue) {
+ current = newValue
+ },
+ }
+}
diff --git a/packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte b/packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte
index 8a02d39a7f..215f1619ca 100644
--- a/packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte
+++ b/packages/svelte-query-persist-client/tests/AwaitOnSuccess/AwaitOnSuccess.svelte
@@ -1,15 +1,15 @@
data: {query.data ?? 'undefined'}
fetchStatus: {query.fetchStatus}
-fetched: {fetched}
diff --git a/packages/svelte-query-persist-client/tests/FreshData/Provider.svelte b/packages/svelte-query-persist-client/tests/FreshData/Provider.svelte
index 3859dbc30e..70a9ea483f 100644
--- a/packages/svelte-query-persist-client/tests/FreshData/Provider.svelte
+++ b/packages/svelte-query-persist-client/tests/FreshData/Provider.svelte
@@ -3,18 +3,17 @@
import FreshData from './FreshData.svelte'
import type { OmitKeyof, QueryClient } from '@tanstack/svelte-query'
import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core'
- import type { StatusResult } from '../utils.svelte.js'
+ import type { StatelessRef, StatusResult } from '../utils.svelte.js'
interface Props {
queryClient: QueryClient
persistOptions: OmitKeyof
- states: { value: Array> }
- fetched: boolean
+ states: StatelessRef>>
}
- let { queryClient, persistOptions, states, fetched }: Props = $props()
+ let { queryClient, persistOptions, states }: Props = $props()
-
+
diff --git a/packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte b/packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte
index ff3397bd2d..20a692f11b 100644
--- a/packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte
+++ b/packages/svelte-query-persist-client/tests/InitialData/InitialData.svelte
@@ -1,10 +1,10 @@
diff --git a/packages/svelte-query-persist-client/tests/InitialData/Provider.svelte b/packages/svelte-query-persist-client/tests/InitialData/Provider.svelte
index b9d600d0df..a50338006a 100644
--- a/packages/svelte-query-persist-client/tests/InitialData/Provider.svelte
+++ b/packages/svelte-query-persist-client/tests/InitialData/Provider.svelte
@@ -3,12 +3,12 @@
import InitialData from './InitialData.svelte'
import type { OmitKeyof, QueryClient } from '@tanstack/svelte-query'
import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core'
- import type { StatusResult } from '../utils.svelte.js'
+ import type { StatelessRef, StatusResult } from '../utils.svelte.js'
interface Props {
queryClient: QueryClient
persistOptions: OmitKeyof
- states: { value: Array> }
+ states: StatelessRef>>
}
let { queryClient, persistOptions, states }: Props = $props()
diff --git a/packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte b/packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte
index 51fc2b0e50..a6ef7b3214 100644
--- a/packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte
+++ b/packages/svelte-query-persist-client/tests/OnSuccess/OnSuccess.svelte
@@ -1,13 +1,9 @@
diff --git a/packages/svelte-query-persist-client/tests/PersistQueryClientProvider.svelte.test.ts b/packages/svelte-query-persist-client/tests/PersistQueryClientProvider.svelte.test.ts
index d49cce5af4..7cd7ac31ab 100644
--- a/packages/svelte-query-persist-client/tests/PersistQueryClientProvider.svelte.test.ts
+++ b/packages/svelte-query-persist-client/tests/PersistQueryClientProvider.svelte.test.ts
@@ -8,8 +8,7 @@ import InitialData from './InitialData/Provider.svelte'
import RemoveCache from './RemoveCache/Provider.svelte'
import RestoreCache from './RestoreCache/Provider.svelte'
import UseQueries from './UseQueries/Provider.svelte'
-import { createQueryClient, ref, sleep } from './utils.svelte.js'
-
+import { StatelessRef, createQueryClient, sleep } from './utils.svelte.js'
import type {
PersistedClient,
Persister,
@@ -24,8 +23,7 @@ const createMockPersister = (): Persister => {
storedState = persistClient
},
async restoreClient() {
- await sleep(5)
- return storedState
+ return Promise.resolve(storedState)
},
removeClient() {
storedState = undefined
@@ -44,8 +42,7 @@ const createMockErrorPersister = (
// noop
},
async restoreClient() {
- await sleep(5)
- throw error
+ return Promise.reject(error)
},
removeClient,
},
@@ -54,7 +51,7 @@ const createMockErrorPersister = (
describe('PersistQueryClientProvider', () => {
test('restores cache from persister', async () => {
- let states = ref>>([])
+ const states = new StatelessRef>>([])
const queryClient = createQueryClient()
await queryClient.prefetchQuery({
@@ -80,41 +77,29 @@ describe('PersistQueryClientProvider', () => {
await waitFor(() => rendered.getByText('hydrated'))
await waitFor(() => rendered.getByText('fetched'))
- expect(states.value).toHaveLength(3)
+ expect(states.current).toHaveLength(3)
- expect(states.value[0]).toMatchObject({
+ expect(states.current[0]).toMatchObject({
status: 'pending',
fetchStatus: 'idle',
data: undefined,
})
- expect(states.value[1]).toMatchObject({
+ expect(states.current[1]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})
- expect(states.value[2]).toMatchObject({
+ expect(states.current[2]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'fetched',
})
-
- /* expect(states[3]).toMatchObject({
- status: 'success',
- fetchStatus: 'fetching',
- data: 'hydrated',
- })
-
- expect(states[4]).toMatchObject({
- status: 'success',
- fetchStatus: 'idle',
- data: 'fetched',
- }) */
})
test('should also put useQueries into idle state', async () => {
- let states = ref>>([])
+ const states = new StatelessRef>>([])
const queryClient = createQueryClient()
await queryClient.prefetchQuery({
@@ -140,21 +125,21 @@ describe('PersistQueryClientProvider', () => {
await waitFor(() => rendered.getByText('hydrated'))
await waitFor(() => rendered.getByText('fetched'))
- expect(states.value).toHaveLength(3)
+ expect(states.current).toHaveLength(3)
- expect(states.value[0]).toMatchObject({
+ expect(states.current[0]).toMatchObject({
status: 'pending',
fetchStatus: 'idle',
data: undefined,
})
- expect(states.value[1]).toMatchObject({
+ expect(states.current[1]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})
- expect(states.value[2]).toMatchObject({
+ expect(states.current[2]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'fetched',
@@ -162,7 +147,7 @@ describe('PersistQueryClientProvider', () => {
})
test('should show initialData while restoring', async () => {
- let states = ref>>([])
+ const states = new StatelessRef>>([])
const queryClient = createQueryClient()
await queryClient.prefetchQuery({
@@ -188,21 +173,21 @@ describe('PersistQueryClientProvider', () => {
await waitFor(() => rendered.getByText('hydrated'))
await waitFor(() => rendered.getByText('fetched'))
- expect(states.value).toHaveLength(3)
+ expect(states.current).toHaveLength(3)
- expect(states.value[0]).toMatchObject({
+ expect(states.current[0]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'initial',
})
- expect(states.value[1]).toMatchObject({
+ expect(states.current[1]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})
- expect(states.value[2]).toMatchObject({
+ expect(states.current[2]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'fetched',
@@ -210,7 +195,7 @@ describe('PersistQueryClientProvider', () => {
})
test('should not refetch after restoring when data is fresh', async () => {
- let states = ref>>([])
+ const states = new StatelessRef>>([])
const queryClient = createQueryClient()
await queryClient.prefetchQuery({
@@ -224,31 +209,31 @@ describe('PersistQueryClientProvider', () => {
queryClient.clear()
- const fetched = $state(false)
-
const rendered = render(FreshData, {
props: {
queryClient,
persistOptions: { persister },
states,
- fetched,
},
})
await waitFor(() => rendered.getByText('data: undefined'))
await waitFor(() => rendered.getByText('data: hydrated'))
+ await expect(
+ waitFor(() => rendered.getByText('data: fetched'), {
+ timeout: 100,
+ }),
+ ).rejects.toThrowError()
- expect(fetched).toBe(false)
+ expect(states.current).toHaveLength(2)
- expect(states.value).toHaveLength(2)
-
- expect(states.value[0]).toMatchObject({
+ expect(states.current[0]).toMatchObject({
status: 'pending',
fetchStatus: 'idle',
data: undefined,
})
- expect(states.value[1]).toMatchObject({
+ expect(states.current[1]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'hydrated',
@@ -279,7 +264,6 @@ describe('PersistQueryClientProvider', () => {
})
expect(onSuccess).toHaveBeenCalledTimes(0)
-
await waitFor(() => rendered.getByText('hydrated'))
expect(onSuccess).toHaveBeenCalledTimes(1)
await waitFor(() => rendered.getByText('fetched'))
@@ -298,7 +282,7 @@ describe('PersistQueryClientProvider', () => {
queryClient.clear()
- let states: Array = $state([])
+ const states = new StatelessRef>([])
const rendered = render(AwaitOnSuccess, {
props: {
@@ -306,9 +290,9 @@ describe('PersistQueryClientProvider', () => {
persistOptions: { persister },
states,
onSuccess: async () => {
- states.push('onSuccess')
+ states.current.push('onSuccess')
await sleep(5)
- states.push('onSuccess done')
+ states.current.push('onSuccess done')
},
},
})
@@ -316,7 +300,7 @@ describe('PersistQueryClientProvider', () => {
await waitFor(() => rendered.getByText('hydrated'))
await waitFor(() => rendered.getByText('fetched'))
- expect(states).toEqual([
+ expect(states.current).toEqual([
'onSuccess',
'onSuccess done',
'fetching',
@@ -325,11 +309,12 @@ describe('PersistQueryClientProvider', () => {
})
test('should remove cache after non-successful restoring', async () => {
- const consoleMock = vi.spyOn(console, 'error')
+ const consoleMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => undefined)
const consoleWarn = vi
.spyOn(console, 'warn')
.mockImplementation(() => undefined)
- consoleMock.mockImplementation(() => undefined)
const queryClient = createQueryClient()
const removeClient = vi.fn()
diff --git a/packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte b/packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte
index 51fc2b0e50..a6ef7b3214 100644
--- a/packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte
+++ b/packages/svelte-query-persist-client/tests/RemoveCache/RemoveCache.svelte
@@ -1,13 +1,9 @@
diff --git a/packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte b/packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte
index cfbf97767c..e89cdbafef 100644
--- a/packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte
+++ b/packages/svelte-query-persist-client/tests/RestoreCache/Provider.svelte
@@ -3,12 +3,12 @@
import RestoreCache from './RestoreCache.svelte'
import type { OmitKeyof, QueryClient } from '@tanstack/svelte-query'
import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core'
- import type { StatusResult } from '../utils.svelte.js'
+ import type { StatelessRef, StatusResult } from '../utils.svelte.js'
interface Props {
queryClient: QueryClient
persistOptions: OmitKeyof
- states: { value: Array> }
+ states: StatelessRef>>
}
let { queryClient, persistOptions, states }: Props = $props()
diff --git a/packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte b/packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte
index 362f39ea60..79b9b6add5 100644
--- a/packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte
+++ b/packages/svelte-query-persist-client/tests/RestoreCache/RestoreCache.svelte
@@ -1,22 +1,19 @@
diff --git a/packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte b/packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte
index de1a961a5f..b5a3857bf7 100644
--- a/packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte
+++ b/packages/svelte-query-persist-client/tests/UseQueries/Provider.svelte
@@ -3,12 +3,12 @@
import UseQueries from './UseQueries.svelte'
import type { OmitKeyof, QueryClient } from '@tanstack/svelte-query'
import type { PersistQueryClientOptions } from '@tanstack/query-persist-client-core'
- import type { StatusResult } from '../utils.svelte.js'
+ import type { StatelessRef, StatusResult } from '../utils.svelte.js'
interface Props {
queryClient: QueryClient
persistOptions: OmitKeyof
- states: { value: Array> }
+ states: StatelessRef>>
}
let { queryClient, persistOptions, states }: Props = $props()
diff --git a/packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte b/packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte
index 122d3da254..4d646ac8cf 100644
--- a/packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte
+++ b/packages/svelte-query-persist-client/tests/UseQueries/UseQueries.svelte
@@ -1,26 +1,23 @@
diff --git a/packages/svelte-query-persist-client/tests/utils.svelte.ts b/packages/svelte-query-persist-client/tests/utils.svelte.ts
index 8e59db6139..d1565f08f0 100644
--- a/packages/svelte-query-persist-client/tests/utils.svelte.ts
+++ b/packages/svelte-query-persist-client/tests/utils.svelte.ts
@@ -1,5 +1,4 @@
import { QueryClient } from '@tanstack/svelte-query'
-
import type { QueryClientConfig } from '@tanstack/svelte-query'
export function createQueryClient(config?: QueryClientConfig): QueryClient {
@@ -18,15 +17,9 @@ export type StatusResult = {
data: T | undefined
}
-export function ref(initial: T) {
- let value = $state(initial)
-
- return {
- get value() {
- return value
- },
- set value(newValue) {
- value = newValue
- },
+export class StatelessRef {
+ current: T
+ constructor(value: T) {
+ this.current = value
}
}
diff --git a/packages/svelte-query-persist-client/vite.config.ts b/packages/svelte-query-persist-client/vite.config.ts
index 54e9cf7efe..facb2d7b76 100644
--- a/packages/svelte-query-persist-client/vite.config.ts
+++ b/packages/svelte-query-persist-client/vite.config.ts
@@ -21,7 +21,6 @@ export default defineConfig({
watch: false,
environment: 'jsdom',
setupFiles: ['./tests/test-setup.ts'],
- coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] },
typecheck: { enabled: true },
restoreMocks: true,
},
diff --git a/packages/svelte-query/package.json b/packages/svelte-query/package.json
index 16442e46a9..cbf9dba5ca 100644
--- a/packages/svelte-query/package.json
+++ b/packages/svelte-query/package.json
@@ -14,6 +14,12 @@
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
+ "keywords": [
+ "tanstack",
+ "query",
+ "svelte",
+ "swr"
+ ],
"scripts": {
"clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts",
"compile": "tsc --build",
@@ -54,6 +60,6 @@
"svelte-check": "^4.1.5"
},
"peerDependencies": {
- "svelte": "^5.0.0"
+ "svelte": "^5.7.0"
}
}
diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts
new file mode 100644
index 0000000000..080a9092e8
--- /dev/null
+++ b/packages/svelte-query/src/containers.svelte.ts
@@ -0,0 +1,123 @@
+import { createSubscriber } from 'svelte/reactivity'
+
+type VoidFn = () => void
+type Subscriber = (update: VoidFn) => void | VoidFn
+
+export type Box = { current: T }
+
+export class ReactiveValue implements Box {
+ #fn
+ #subscribe
+
+ constructor(fn: () => T, onSubscribe: Subscriber) {
+ this.#fn = fn
+ this.#subscribe = createSubscriber((update) => onSubscribe(update))
+ }
+
+ get current() {
+ this.#subscribe()
+ return this.#fn()
+ }
+}
+
+/**
+ * Makes all of the top-level keys of an object into $state.raw fields whose initial values
+ * are the same as in the original object. Does not mutate the original object. Provides an `update`
+ * function that _can_ (but does not have to be) be used to replace all of the object's top-level keys
+ * with the values of the new object, while maintaining the original root object's reference.
+ */
+export function createRawRef>(
+ init: T,
+): [T, (newValue: T) => void] {
+ const refObj = (Array.isArray(init) ? [] : {}) as T
+ const hiddenKeys = new Set()
+ const out = new Proxy(refObj, {
+ set(target, prop, value, receiver) {
+ hiddenKeys.delete(prop)
+ if (prop in target) {
+ return Reflect.set(target, prop, value, receiver)
+ }
+ let state = $state.raw(value)
+ Object.defineProperty(target, prop, {
+ configurable: true,
+ enumerable: true,
+ get: () => {
+ // If this is a lazy value, we need to call it.
+ // We can't do something like typeof state === 'function'
+ // because the value could actually be a function that we don't want to call.
+ return state && isBranded(state) ? state() : state
+ },
+ set: (v) => {
+ state = v
+ },
+ })
+ return true
+ },
+ has: (target, prop) => {
+ if (hiddenKeys.has(prop)) {
+ return false
+ }
+ return prop in target
+ },
+ ownKeys(target) {
+ return Reflect.ownKeys(target).filter((key) => !hiddenKeys.has(key))
+ },
+ getOwnPropertyDescriptor(target, prop) {
+ if (hiddenKeys.has(prop)) {
+ return undefined
+ }
+ return Reflect.getOwnPropertyDescriptor(target, prop)
+ },
+ deleteProperty(target, prop) {
+ if (prop in target) {
+ // @ts-expect-error
+ // We need to set the value to undefined to signal to the listeners that the value has changed.
+ // If we just deleted it, the reactivity system wouldn't have any idea that the value was gone.
+ target[prop] = undefined
+ hiddenKeys.add(prop)
+ if (Array.isArray(target)) {
+ target.length--
+ }
+ return true
+ }
+ return false
+ },
+ })
+
+ function update(newValue: T) {
+ const existingKeys = Object.keys(out)
+ const newKeys = Object.keys(newValue)
+ const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key))
+ for (const key of keysToRemove) {
+ // @ts-expect-error
+ delete out[key]
+ }
+ for (const key of newKeys) {
+ // @ts-expect-error
+ // This craziness is required because Tanstack Query defines getters for all of the keys on the object.
+ // These getters track property access, so if we access all of them here, we'll end up tracking everything.
+ // So we wrap the property access in a special function that we can identify later to lazily access the value.
+ // (See above)
+ out[key] = brand(() => newValue[key])
+ }
+ }
+
+ // we can't pass `init` directly into the proxy because it'll never set the state fields
+ // (because (prop in target) will always be true)
+ update(init)
+
+ return [out, update]
+}
+
+const lazyBrand = Symbol('LazyValue')
+type Branded unknown> = T & { [lazyBrand]: true }
+
+function brand unknown>(fn: T): Branded {
+ // @ts-expect-error
+ fn[lazyBrand] = true
+ return fn as Branded
+}
+
+function isBranded unknown>(fn: T): fn is Branded {
+ return Boolean((fn as Branded)[lazyBrand])
+}
diff --git a/packages/svelte-query/src/context.ts b/packages/svelte-query/src/context.ts
index 0676181f57..27595517f5 100644
--- a/packages/svelte-query/src/context.ts
+++ b/packages/svelte-query/src/context.ts
@@ -1,18 +1,19 @@
import { getContext, setContext } from 'svelte'
import type { QueryClient } from '@tanstack/query-core'
+import type { Box } from './containers.svelte'
-const _contextKey = '$$_queryClient'
+const _contextKey = Symbol('QueryClient')
/** Retrieves a Client from Svelte's context */
export const getQueryClientContext = (): QueryClient => {
- const client = getContext(_contextKey)
+ const client = getContext(_contextKey)
if (!client) {
throw new Error(
'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?',
)
}
- return client as QueryClient
+ return client
}
/** Sets a QueryClient on Svelte's context */
@@ -20,21 +21,21 @@ export const setQueryClientContext = (client: QueryClient): void => {
setContext(_contextKey, client)
}
-const _isRestoringContextKey = '$$_isRestoring'
+const _isRestoringContextKey = Symbol('isRestoring')
/** Retrieves a `isRestoring` from Svelte's context */
-export const getIsRestoringContext = (): (() => boolean) => {
+export const getIsRestoringContext = (): Box => {
try {
- const isRestoring = getContext<(() => boolean) | undefined>(
+ const isRestoring = getContext | undefined>(
_isRestoringContextKey,
)
- return isRestoring ?? (() => false)
+ return isRestoring ?? { current: false }
} catch (error) {
- return () => false
+ return { current: false }
}
}
/** Sets a `isRestoring` on Svelte's context */
-export const setIsRestoringContext = (isRestoring: () => boolean): void => {
+export const setIsRestoringContext = (isRestoring: Box): void => {
setContext(_isRestoringContextKey, isRestoring)
}
diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts
index 6f5e4a1b07..8307f5e40f 100644
--- a/packages/svelte-query/src/createBaseQuery.svelte.ts
+++ b/packages/svelte-query/src/createBaseQuery.svelte.ts
@@ -1,18 +1,20 @@
-import { notifyManager } from '@tanstack/query-core'
+import { untrack } from 'svelte'
import { useIsRestoring } from './useIsRestoring.js'
import { useQueryClient } from './useQueryClient.js'
+import { createRawRef } from './containers.svelte.js'
+import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
import type {
+ Accessor,
CreateBaseQueryOptions,
CreateBaseQueryResult,
- FunctionedParams,
} from './types.js'
-import type {
- QueryClient,
- QueryKey,
- QueryObserver,
- QueryObserverResult,
-} from '@tanstack/query-core'
+/**
+ * Base implementation for `createQuery` and `createInfiniteQuery`
+ * @param options - A function that returns query options
+ * @param Observer - The observer from query-core
+ * @param queryClient - Custom query client which overrides provider
+ */
export function createBaseQuery<
TQueryFnData,
TError,
@@ -20,64 +22,62 @@ export function createBaseQuery<
TQueryData,
TQueryKey extends QueryKey,
>(
- options: FunctionedParams<
+ options: Accessor<
CreateBaseQueryOptions
>,
Observer: typeof QueryObserver,
- queryClient?: QueryClient,
+ queryClient?: Accessor,
): CreateBaseQueryResult {
/** Load query client */
- const client = useQueryClient(queryClient)
+ const client = $derived(useQueryClient(queryClient?.()))
const isRestoring = useIsRestoring()
- /** Creates a store that has the default options applied */
- const defaultedOptions = $derived(() => {
- const defaultOptions = client.defaultQueryOptions(options())
- defaultOptions._optimisticResults = isRestoring()
- ? 'isRestoring'
- : 'optimistic'
- defaultOptions.structuralSharing = false
- return defaultOptions
+ const resolvedOptions = $derived.by(() => {
+ const opts = client.defaultQueryOptions(options())
+ opts._optimisticResults = isRestoring.current ? 'isRestoring' : 'optimistic'
+ return opts
})
/** Creates the observer */
- const observer = new Observer<
- TQueryFnData,
- TError,
- TData,
- TQueryData,
- TQueryKey
- >(client, defaultedOptions())
-
- const result = $state>(
- observer.getOptimisticResult(defaultedOptions()),
+ const observer = $derived(
+ new Observer(
+ client,
+ untrack(() => resolvedOptions),
+ ),
)
- function updateResult(r: QueryObserverResult) {
- Object.assign(result, r)
+ function createResult() {
+ const result = observer.getOptimisticResult(resolvedOptions)
+ return !resolvedOptions.notifyOnChangeProps
+ ? observer.trackResult(result)
+ : result
}
+ const [query, update] = createRawRef(
+ // svelte-ignore state_referenced_locally - intentional, initial value
+ createResult(),
+ )
$effect(() => {
- const unsubscribe = isRestoring()
+ const unsubscribe = isRestoring.current
? () => undefined
- : observer.subscribe(() => {
- notifyManager.batchCalls(() => {
- updateResult(observer.getOptimisticResult(defaultedOptions()))
- })()
- })
-
+ : observer.subscribe(() => update(createResult()))
observer.updateResult()
- return () => unsubscribe()
+ return unsubscribe
})
- /** Subscribe to changes in result and defaultedOptionsStore */
$effect.pre(() => {
- observer.setOptions(defaultedOptions())
- updateResult(observer.getOptimisticResult(defaultedOptions()))
+ observer.setOptions(resolvedOptions)
+ // The only reason this is necessary is because of `isRestoring`.
+ // Because we don't subscribe while restoring, the following can occur:
+ // - `isRestoring` is true
+ // - `isRestoring` becomes false
+ // - `observer.subscribe` and `observer.updateResult` is called in the above effect,
+ // but the subsequent `fetch` has already completed
+ // - `result` misses the intermediate restored-but-not-fetched state
+ //
+ // this could technically be its own effect but that doesn't seem necessary
+ update(createResult())
})
- // Handle result property usage tracking
- return !defaultedOptions().notifyOnChangeProps
- ? observer.trackResult(result)
- : result
+ return query
}
diff --git a/packages/svelte-query/src/createInfiniteQuery.ts b/packages/svelte-query/src/createInfiniteQuery.ts
index b12d556fa3..0e106c6e71 100644
--- a/packages/svelte-query/src/createInfiniteQuery.ts
+++ b/packages/svelte-query/src/createInfiniteQuery.ts
@@ -8,9 +8,9 @@ import type {
QueryObserver,
} from '@tanstack/query-core'
import type {
+ Accessor,
CreateInfiniteQueryOptions,
CreateInfiniteQueryResult,
- FunctionedParams,
} from './types.js'
export function createInfiniteQuery<
@@ -20,7 +20,7 @@ export function createInfiniteQuery<
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
- options: FunctionedParams<
+ options: Accessor<
CreateInfiniteQueryOptions<
TQueryFnData,
TError,
@@ -30,7 +30,7 @@ export function createInfiniteQuery<
TPageParam
>
>,
- queryClient?: QueryClient,
+ queryClient?: Accessor,
): CreateInfiniteQueryResult {
return createBaseQuery(
options,
diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts
index 3f0dfcecc3..62e39d98e6 100644
--- a/packages/svelte-query/src/createMutation.svelte.ts
+++ b/packages/svelte-query/src/createMutation.svelte.ts
@@ -3,26 +3,28 @@ import { onDestroy } from 'svelte'
import { MutationObserver, notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './useQueryClient.js'
import type {
+ Accessor,
CreateMutateFunction,
CreateMutationOptions,
CreateMutationResult,
- FunctionedParams,
} from './types.js'
import type { DefaultError, QueryClient } from '@tanstack/query-core'
+/**
+ * @param options - A function that returns mutation options
+ * @param queryClient - Custom query client which overrides provider
+ */
export function createMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
- options: FunctionedParams<
- CreateMutationOptions
- >,
- queryClient?: QueryClient,
+ options: Accessor>,
+ queryClient?: Accessor,
): CreateMutationResult {
- const client = useQueryClient(queryClient)
+ const client = useQueryClient(queryClient?.())
const observer = $derived(
new MutationObserver(
diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts
index 920aac6979..e546dc600d 100644
--- a/packages/svelte-query/src/createQueries.svelte.ts
+++ b/packages/svelte-query/src/createQueries.svelte.ts
@@ -1,31 +1,34 @@
+import { QueriesObserver } from '@tanstack/query-core'
import { untrack } from 'svelte'
-import { QueriesObserver, notifyManager } from '@tanstack/query-core'
import { useIsRestoring } from './useIsRestoring.js'
+import { createRawRef } from './containers.svelte.js'
import { useQueryClient } from './useQueryClient.js'
-import type { FunctionedParams } from './types.js'
+import type {
+ Accessor,
+ CreateQueryOptions,
+ CreateQueryResult,
+ DefinedCreateQueryResult,
+} from './types.js'
import type {
DefaultError,
- DefinedQueryObserverResult,
OmitKeyof,
QueriesObserverOptions,
QueriesPlaceholderDataFunction,
QueryClient,
QueryFunction,
QueryKey,
- QueryObserverOptions,
- QueryObserverResult,
ThrowOnError,
} from '@tanstack/query-core'
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// `placeholderData` function always gets undefined passed
-type QueryObserverOptionsForCreateQueries<
+type CreateQueryOptionsForCreateQueries<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
- QueryObserverOptions,
+ CreateQueryOptions,
'placeholderData'
> & {
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction
@@ -35,60 +38,60 @@ type QueryObserverOptionsForCreateQueries<
type MAXIMUM_DEPTH = 20
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
-type SkipTokenForUseQueries = symbol
+type SkipTokenForCreateQueries = symbol
-type GetQueryObserverOptionsForCreateQueries =
+type GetCreateQueryOptionsForCreateQueries =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: T extends { data: infer TData; error?: infer TError }
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: T extends [infer TQueryFnData, infer TError]
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: T extends [infer TQueryFnData]
- ? QueryObserverOptionsForCreateQueries
+ ? CreateQueryOptionsForCreateQueries
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction
- | SkipTokenForUseQueries
+ | SkipTokenForCreateQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError
}
- ? QueryObserverOptionsForCreateQueries<
+ ? CreateQueryOptionsForCreateQueries<
TQueryFnData,
unknown extends TError ? DefaultError : TError,
unknown extends TData ? TQueryFnData : TData,
TQueryKey
>
: // Fallback
- QueryObserverOptionsForCreateQueries
+ CreateQueryOptionsForCreateQueries
-// A defined initialData setting should return a DefinedQueryObserverResult rather than CreateQueryResult
+// A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult
type GetDefinedOrUndefinedQueryResult = T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
- ? QueryObserverResult
+ ? CreateQueryResult
: TInitialData extends TData
- ? DefinedQueryObserverResult
+ ? DefinedCreateQueryResult
: TInitialData extends () => infer TInitialDataResult
? unknown extends TInitialDataResult
- ? QueryObserverResult
+ ? CreateQueryResult
: TInitialDataResult extends TData
- ? DefinedQueryObserverResult
- : QueryObserverResult
- : QueryObserverResult
- : QueryObserverResult
+ ? DefinedCreateQueryResult
+ : CreateQueryResult
+ : CreateQueryResult
+ : CreateQueryResult
type GetCreateQueryResult =
// Part 1: responsible for mapping explicit type parameter to function result, if object
@@ -109,7 +112,7 @@ type GetCreateQueryResult =
T extends {
queryFn?:
| QueryFunction
- | SkipTokenForUseQueries
+ | SkipTokenForCreateQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError
}
@@ -119,7 +122,7 @@ type GetCreateQueryResult =
unknown extends TError ? DefaultError : TError
>
: // Fallback
- QueryObserverResult
+ CreateQueryResult
/**
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
@@ -129,15 +132,15 @@ export type QueriesOptions<
TResults extends Array = [],
TDepth extends ReadonlyArray = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
- ? Array
+ ? Array
: T extends []
? []
: T extends [infer Head]
- ? [...TResults, GetQueryObserverOptionsForCreateQueries]
+ ? [...TResults, GetCreateQueryOptionsForCreateQueries]
: T extends [infer Head, ...infer Tails]
? QueriesOptions<
[...Tails],
- [...TResults, GetQueryObserverOptionsForCreateQueries],
+ [...TResults, GetCreateQueryOptionsForCreateQueries],
[...TDepth, 1]
>
: ReadonlyArray extends T
@@ -145,7 +148,7 @@ export type QueriesOptions<
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
- QueryObserverOptionsForCreateQueries<
+ CreateQueryOptionsForCreateQueries<
infer TQueryFnData,
infer TError,
infer TData,
@@ -153,7 +156,7 @@ export type QueriesOptions<
>
>
? Array<
- QueryObserverOptionsForCreateQueries<
+ CreateQueryOptionsForCreateQueries<
TQueryFnData,
TError,
TData,
@@ -161,7 +164,7 @@ export type QueriesOptions<
>
>
: // Fallback
- Array
+ Array
/**
* QueriesResults reducer recursively maps type param to results
@@ -171,7 +174,7 @@ export type QueriesResults<
TResults extends Array = [],
TDepth extends ReadonlyArray = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
- ? Array
+ ? Array
: T extends []
? []
: T extends [infer Head]
@@ -188,77 +191,64 @@ export function createQueries<
T extends Array,
TCombinedResult = QueriesResults,
>(
- {
- queries,
- ...options
- }: {
+ createQueriesOptions: Accessor<{
queries:
- | FunctionedParams<[...QueriesOptions]>
- | FunctionedParams<
- [...{ [K in keyof T]: GetQueryObserverOptionsForCreateQueries }]
- >
+ | readonly [...QueriesOptions]
+ | readonly [
+ ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries },
+ ]
combine?: (result: QueriesResults) => TCombinedResult
- },
- queryClient?: QueryClient,
+ }>,
+ queryClient?: Accessor,
): TCombinedResult {
- const client = useQueryClient(queryClient)
+ const client = $derived(useQueryClient(queryClient?.()))
const isRestoring = useIsRestoring()
- const defaultedQueries = $derived(() => {
- return queries().map((opts) => {
- const defaultedOptions = client.defaultQueryOptions(opts)
+ const { queries, combine } = $derived.by(createQueriesOptions)
+ const resolvedQueryOptions = $derived(
+ queries.map((opts) => {
+ const resolvedOptions = client.defaultQueryOptions(opts)
// Make sure the results are already in fetching state before subscribing or updating options
- defaultedOptions._optimisticResults = isRestoring()
+ resolvedOptions._optimisticResults = isRestoring.current
? 'isRestoring'
: 'optimistic'
- return defaultedOptions as QueryObserverOptions
- })
- })
-
- const observer = new QueriesObserver(
- client,
- defaultedQueries(),
- options as QueriesObserverOptions,
+ return resolvedOptions
+ }),
)
- const [_, getCombinedResult, trackResult] = $derived(
- observer.getOptimisticResult(
- defaultedQueries(),
- (options as QueriesObserverOptions).combine,
+ const observer = $derived(
+ new QueriesObserver(
+ client,
+ untrack(() => resolvedQueryOptions),
+ untrack(() => combine as QueriesObserverOptions),
),
)
- $effect(() => {
- // 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(),
- options as QueriesObserverOptions,
+ function createResult() {
+ const [_, getCombinedResult, trackResult] = observer.getOptimisticResult(
+ resolvedQueryOptions,
+ combine as QueriesObserverOptions['combine'],
)
- })
+ return getCombinedResult(trackResult())
+ }
- let result = $state(getCombinedResult(trackResult()))
+ // @ts-expect-error - the crazy-complex TCombinedResult type doesn't like being called an array
+ // svelte-ignore state_referenced_locally
+ const [results, update] = createRawRef(createResult())
$effect(() => {
- if (isRestoring()) {
- return () => null
- }
- untrack(() => {
- // @ts-expect-error
- Object.assign(result, getCombinedResult(trackResult()))
- })
+ const unsubscribe = isRestoring.current
+ ? () => undefined
+ : observer.subscribe(() => update(createResult()))
+ return unsubscribe
+ })
- return observer.subscribe((_result) => {
- notifyManager.batchCalls(() => {
- const res = observer.getOptimisticResult(
- defaultedQueries(),
- (options as QueriesObserverOptions).combine,
- )
- // @ts-expect-error
- Object.assign(result, res[1](res[2]()))
- })()
- })
+ $effect.pre(() => {
+ observer.setQueries(resolvedQueryOptions, {
+ combine,
+ } as QueriesObserverOptions)
+ update(createResult())
})
- return result
+ return results
}
diff --git a/packages/svelte-query/src/createQuery.ts b/packages/svelte-query/src/createQuery.ts
index 79b6782b2f..bf7efe81a7 100644
--- a/packages/svelte-query/src/createQuery.ts
+++ b/packages/svelte-query/src/createQuery.ts
@@ -2,10 +2,10 @@ import { QueryObserver } from '@tanstack/query-core'
import { createBaseQuery } from './createBaseQuery.svelte.js'
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
import type {
+ Accessor,
CreateQueryOptions,
CreateQueryResult,
DefinedCreateQueryResult,
- FunctionedParams,
} from './types.js'
import type {
DefinedInitialDataOptions,
@@ -18,11 +18,11 @@ export function createQuery<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
- options: FunctionedParams<
- DefinedInitialDataOptions
+ options: Accessor<
+ UndefinedInitialDataOptions
>,
- queryClient?: QueryClient,
-): DefinedCreateQueryResult
+ queryClient?: Accessor,
+): CreateQueryResult
export function createQuery<
TQueryFnData = unknown,
@@ -30,27 +30,25 @@ export function createQuery<
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
- options: FunctionedParams<
- UndefinedInitialDataOptions
+ options: Accessor<
+ DefinedInitialDataOptions
>,
- queryClient?: QueryClient,
-): CreateQueryResult
+ queryClient?: Accessor,
+): DefinedCreateQueryResult
export function createQuery<
- TQueryFnData = unknown,
+ TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
- options: FunctionedParams<
- CreateQueryOptions
- >,
- queryClient?: QueryClient,
+ options: Accessor>,
+ queryClient?: Accessor,
): CreateQueryResult
export function createQuery(
- options: FunctionedParams,
- queryClient?: QueryClient,
+ options: Accessor,
+ queryClient?: Accessor,
) {
return createBaseQuery(options, QueryObserver, queryClient)
}
diff --git a/packages/svelte-query/src/types.ts b/packages/svelte-query/src/types.ts
index 05116a8786..1c6cc4d20e 100644
--- a/packages/svelte-query/src/types.ts
+++ b/packages/svelte-query/src/types.ts
@@ -17,7 +17,7 @@ import type {
QueryObserverResult,
} from '@tanstack/query-core'
-export type FunctionedParams = () => T
+export type Accessor = () => T
/** Options for createBaseQuery */
export type CreateBaseQueryOptions<
diff --git a/packages/svelte-query/src/useIsFetching.svelte.ts b/packages/svelte-query/src/useIsFetching.svelte.ts
index 2296301a28..0b8c47e3fd 100644
--- a/packages/svelte-query/src/useIsFetching.svelte.ts
+++ b/packages/svelte-query/src/useIsFetching.svelte.ts
@@ -1,23 +1,16 @@
-import { onDestroy } from 'svelte'
+import { ReactiveValue } from './containers.svelte.js'
import { useQueryClient } from './useQueryClient.js'
import type { QueryClient, QueryFilters } from '@tanstack/query-core'
export function useIsFetching(
filters?: QueryFilters,
queryClient?: QueryClient,
-): () => number {
+): ReactiveValue {
const client = useQueryClient(queryClient)
const queryCache = client.getQueryCache()
- const init = client.isFetching(filters)
- let isFetching = $state(init)
- $effect(() => {
- const unsubscribe = queryCache.subscribe(() => {
- isFetching = client.isFetching(filters)
- })
-
- onDestroy(unsubscribe)
- })
-
- return () => isFetching
+ return new ReactiveValue(
+ () => client.isFetching(filters),
+ (update) => queryCache.subscribe(update),
+ )
}
diff --git a/packages/svelte-query/src/useIsMutating.svelte.ts b/packages/svelte-query/src/useIsMutating.svelte.ts
index 5e7992a93a..21ac56e7a8 100644
--- a/packages/svelte-query/src/useIsMutating.svelte.ts
+++ b/packages/svelte-query/src/useIsMutating.svelte.ts
@@ -1,29 +1,16 @@
-import { notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './useQueryClient.js'
+import { ReactiveValue } from './containers.svelte.js'
import type { MutationFilters, QueryClient } from '@tanstack/query-core'
export function useIsMutating(
filters?: MutationFilters,
queryClient?: QueryClient,
-): () => number {
+): ReactiveValue {
const client = useQueryClient(queryClient)
const cache = client.getMutationCache()
- // isMutating is the prev value initialized on mount *
- let isMutating = client.isMutating(filters)
- const num = $state({ isMutating })
- $effect(() => {
- return cache.subscribe(
- notifyManager.batchCalls(() => {
- const newIisMutating = client.isMutating(filters)
- if (isMutating !== newIisMutating) {
- // * and update with each change
- isMutating = newIisMutating
- num.isMutating = isMutating
- }
- }),
- )
- })
-
- return () => num.isMutating
+ return new ReactiveValue(
+ () => client.isMutating(filters),
+ (update) => cache.subscribe(update),
+ )
}
diff --git a/packages/svelte-query/src/useIsRestoring.ts b/packages/svelte-query/src/useIsRestoring.ts
index f6ee9bb564..99dd4ddacb 100644
--- a/packages/svelte-query/src/useIsRestoring.ts
+++ b/packages/svelte-query/src/useIsRestoring.ts
@@ -1,5 +1,6 @@
import { getIsRestoringContext } from './context.js'
+import type { Box } from './containers.svelte.js'
-export function useIsRestoring(): () => boolean {
+export function useIsRestoring(): Box {
return getIsRestoringContext()
}
diff --git a/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.test.ts b/packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts
similarity index 100%
rename from packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.test.ts
rename to packages/svelte-query/tests/QueryClientProvider/QueryClientProvider.svelte.test.ts
diff --git a/packages/svelte-query/tests/containers.svelte.test.ts b/packages/svelte-query/tests/containers.svelte.test.ts
new file mode 100644
index 0000000000..3511dbb5b5
--- /dev/null
+++ b/packages/svelte-query/tests/containers.svelte.test.ts
@@ -0,0 +1,219 @@
+import { flushSync } from 'svelte'
+import { describe, expect, it } from 'vitest'
+import { createRawRef } from '../src/containers.svelte.js'
+import { withEffectRoot } from './utils.svelte.js'
+
+describe('createRawRef', () => {
+ it('should create a reactive reference', () => {
+ const [ref, update] = createRawRef({ a: 1, b: 2 })
+
+ expect(ref).toEqual({ a: 1, b: 2 })
+
+ update({ a: 3, b: 4 })
+ expect(ref).toEqual({ a: 3, b: 4 })
+
+ ref.a = 5
+ expect(ref).toEqual({ a: 5, b: 4 })
+ })
+
+ it('should handle nested objects', () => {
+ const [ref, update] = createRawRef<{ a: any }>({ a: { b: { c: 1 } } })
+
+ expect(ref).toEqual({ a: { b: { c: 1 } } })
+
+ // update with same structure
+ update({ a: { b: { c: 2 } } })
+ expect(ref).toEqual({ a: { b: { c: 2 } } })
+
+ ref.a.b.c = 3
+ expect(ref).toEqual({ a: { b: { c: 3 } } })
+
+ // update with different structure should wipe out everything below the first level
+ update({ a: { b: 3 } })
+ expect(ref).toEqual({ a: { b: 3 } })
+ })
+
+ it('should remove properties when a new object is assigned', () => {
+ const [ref, update] = createRawRef>({
+ a: 1,
+ b: 2,
+ })
+
+ expect(ref).toEqual({ a: 1, b: 2 })
+
+ update({ a: 3 })
+ expect(ref).toEqual({ a: 3 })
+ })
+
+ it(
+ 'should not break reactivity when removing keys',
+ withEffectRoot(() => {
+ const [ref, update] = createRawRef>({ a: 1, b: 2 })
+ const states: Array = []
+ $effect(() => {
+ states.push(ref.b)
+ })
+
+ // these flushSync calls force the effect to run and push the value to the states array
+ flushSync()
+ update({ a: 3 }) // should remove b, and should rerun the effect
+ flushSync()
+ update({ a: 3, b: 4 }) // should add b back, and should rerun the effect
+ flushSync()
+ delete ref.b // should remove b, and should rerun the effect
+ flushSync()
+ delete ref.a // should remove a, and should _not_ rerun the effect
+ expect(states).toEqual([2, undefined, 4, undefined])
+ }),
+ )
+
+ it(
+ 'should correctly trap calls to `in`',
+ withEffectRoot(() => {
+ const [ref, update] = createRawRef>({
+ a: 1,
+ b: 2,
+ })
+
+ expect('b' in ref).toBe(true)
+ delete ref.b
+ expect('b' in ref).toBe(false)
+ update({})
+ expect('a' in ref).toBe(false)
+ update({ a: 1, b: 2 })
+ expect('b' in ref).toBe(true)
+ expect('a' in ref).toBe(true)
+ }),
+ )
+
+ it('should correctly trap calls to `ownKeys`', () => {
+ const [ref, update] = createRawRef>({
+ a: 1,
+ b: 2,
+ })
+
+ expect(Object.keys(ref)).toEqual(['a', 'b'])
+
+ delete ref.b
+ expect(Reflect.ownKeys(ref)).toEqual(['a'])
+
+ update({})
+ expect(Object.keys(ref)).toEqual([])
+
+ update({ a: 1, b: 2 })
+ expect(Object.keys(ref)).toEqual(['a', 'b'])
+ })
+
+ it('should correctly trap calls to `getOwnPropertyDescriptor`', () => {
+ const [ref, update] = createRawRef>({
+ a: 1,
+ b: 2,
+ })
+
+ expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({
+ configurable: true,
+ enumerable: true,
+ get: expect.any(Function),
+ set: expect.any(Function),
+ })
+
+ delete ref.b
+ expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual(undefined)
+
+ update({})
+ expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual(undefined)
+
+ update({ a: 1, b: 2 })
+ expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual({
+ configurable: true,
+ enumerable: true,
+ get: expect.any(Function),
+ set: expect.any(Function),
+ })
+ expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({
+ configurable: true,
+ enumerable: true,
+ get: expect.any(Function),
+ set: expect.any(Function),
+ })
+ })
+
+ it('should lazily access values when using `update`', () => {
+ let aAccessed = false
+ let bAccessed = false
+ const [ref, update] = createRawRef({
+ get a() {
+ aAccessed = true
+ return 1
+ },
+ get b() {
+ bAccessed = true
+ return 2
+ },
+ })
+
+ expect(aAccessed).toBe(false)
+ expect(bAccessed).toBe(false)
+
+ expect(ref.a).toBe(1)
+
+ expect(aAccessed).toBe(true)
+ expect(bAccessed).toBe(false)
+
+ aAccessed = false
+ bAccessed = false
+
+ update({
+ get a() {
+ aAccessed = true
+ return 2
+ },
+ get b() {
+ bAccessed = true
+ return 3
+ },
+ })
+
+ expect(aAccessed).toBe(false)
+ expect(bAccessed).toBe(false)
+
+ expect(ref.a).toBe(2)
+
+ expect(aAccessed).toBe(true)
+ expect(bAccessed).toBe(false)
+ })
+
+ it('should handle arrays', () => {
+ const [ref, update] = createRawRef([1, 2, 3])
+
+ expect(ref).toEqual([1, 2, 3])
+
+ ref[0] = 4
+ expect(ref).toEqual([4, 2, 3])
+
+ update([5, 6])
+ expect(ref).toEqual([5, 6])
+
+ update([7, 8, 9])
+ expect(ref).toEqual([7, 8, 9])
+ })
+
+ it('should behave like a regular object when not using `update`', () => {
+ const [ref] = createRawRef>({ a: 1, b: 2 })
+
+ expect(ref).toEqual({ a: 1, b: 2 })
+
+ ref.a = 3
+ expect(ref).toEqual({ a: 3, b: 2 })
+
+ ref.b = 4
+ expect(ref).toEqual({ a: 3, b: 4 })
+
+ ref.c = 5
+ expect(ref).toEqual({ a: 3, b: 4, c: 5 })
+
+ ref.fn = () => 6
+ expect(ref).toEqual({ a: 3, b: 4, c: 5, fn: expect.any(Function) })
+ expect((ref.fn as () => number)()).toBe(6)
+ })
+})
diff --git a/packages/svelte-query/tests/context/context.test.ts b/packages/svelte-query/tests/context/context.svelte.test.ts
similarity index 100%
rename from packages/svelte-query/tests/context/context.test.ts
rename to packages/svelte-query/tests/context/context.svelte.test.ts
diff --git a/packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte b/packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte
index 5ae5a42579..f3303e9aca 100644
--- a/packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte
+++ b/packages/svelte-query/tests/createInfiniteQuery/BaseExample.svelte
@@ -19,7 +19,7 @@
getNextPageParam: (lastPage) => lastPage + 1,
initialPageParam: 0,
}),
- queryClient,
+ () => queryClient,
)
$effect(() => {
diff --git a/packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte b/packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte
index 9004370f08..626122da75 100644
--- a/packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte
+++ b/packages/svelte-query/tests/createInfiniteQuery/SelectExample.svelte
@@ -19,7 +19,7 @@
getNextPageParam: () => undefined,
initialPageParam: 0,
}),
- queryClient,
+ () => queryClient,
)
$effect(() => {
diff --git a/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.test.ts b/packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts
similarity index 100%
rename from packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.test.ts
rename to packages/svelte-query/tests/createInfiniteQuery/createInfiniteQuery.svelte.test.ts
diff --git a/packages/svelte-query/tests/createMutation/createMutation.test.ts b/packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts
similarity index 100%
rename from packages/svelte-query/tests/createMutation/createMutation.test.ts
rename to packages/svelte-query/tests/createMutation/createMutation.svelte.test.ts
diff --git a/packages/svelte-query/tests/createQueries.svelte.test.ts b/packages/svelte-query/tests/createQueries.svelte.test.ts
new file mode 100644
index 0000000000..2f9582afdc
--- /dev/null
+++ b/packages/svelte-query/tests/createQueries.svelte.test.ts
@@ -0,0 +1,935 @@
+import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
+import { QueryCache, QueryClient, createQueries } from '../src/index.js'
+import { promiseWithResolvers, withEffectRoot } from './utils.svelte.js'
+import type {
+ CreateQueryOptions,
+ CreateQueryResult,
+ QueryFunction,
+ QueryFunctionContext,
+ QueryKey,
+ skipToken,
+} from '../src/index.js'
+
+describe('createQueries', () => {
+ const queryCache = new QueryCache()
+ const queryClient = new QueryClient({ queryCache })
+
+ beforeEach(() => {
+ queryCache.clear()
+ })
+
+ it(
+ 'should return the correct states',
+ withEffectRoot(async () => {
+ const key1 = ['test-1']
+ const key2 = ['test-2']
+ const results: Array> = []
+ const { promise: promise1, resolve: resolve1 } = promiseWithResolvers()
+ const { promise: promise2, resolve: resolve2 } = promiseWithResolvers()
+
+ const result = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => promise1,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => promise2,
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ $effect(() => {
+ results.push([{ ...result[0] }, { ...result[1] }])
+ })
+
+ resolve1(1)
+
+ await vi.waitFor(() => expect(result[0].data).toBe(1))
+
+ resolve2(2)
+ await vi.waitFor(() => expect(result[1].data).toBe(2))
+
+ expect(results.length).toBe(3)
+ expect(results[0]).toMatchObject([
+ { data: undefined },
+ { data: undefined },
+ ])
+ expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
+ expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
+ }),
+ )
+
+ it(
+ 'handles type parameter - tuple of tuples',
+ withEffectRoot(() => {
+ const key1 = ['test-key-1']
+ const key2 = ['test-key-2']
+ const key3 = ['test-key-3']
+
+ const result1 = createQueries<
+ [[number], [string], [Array, boolean]]
+ >(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 1,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key3,
+ queryFn: () => ['string[]'],
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result1[0]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result1[1]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result1[2]).toEqualTypeOf<
+ CreateQueryResult, boolean>
+ >()
+ expectTypeOf(result1[0].data).toEqualTypeOf()
+ expectTypeOf(result1[1].data).toEqualTypeOf()
+ expectTypeOf(result1[2].data).toEqualTypeOf | undefined>()
+ expectTypeOf(result1[2].error).toEqualTypeOf()
+
+ // TData (3rd element) takes precedence over TQueryFnData (1st element)
+ const result2 = createQueries<
+ [[string, unknown, string], [string, unknown, number]]
+ >(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a.toLowerCase()
+ },
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return parseInt(a)
+ },
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result2[0]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result2[1]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result2[0].data).toEqualTypeOf()
+ expectTypeOf(result2[1].data).toEqualTypeOf()
+
+ // types should be enforced
+ createQueries<[[string, unknown, string], [string, boolean, number]]>(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a.toLowerCase()
+ },
+ placeholderData: 'string',
+ // @ts-expect-error (initialData: string)
+ initialData: 123,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return parseInt(a)
+ },
+ placeholderData: 'string',
+ // @ts-expect-error (initialData: string)
+ initialData: 123,
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ // field names should be enforced
+ createQueries<[[string]]>(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+ }),
+ )
+
+ it(
+ 'handles type parameter - tuple of objects',
+ withEffectRoot(() => {
+ const key1 = ['test-key-1']
+ const key2 = ['test-key-2']
+ const key3 = ['test-key-3']
+
+ const result1 = createQueries<
+ [
+ { queryFnData: number },
+ { queryFnData: string },
+ { queryFnData: Array; error: boolean },
+ ]
+ >(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 1,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key3,
+ queryFn: () => ['string[]'],
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result1[0]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result1[1]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result1[2]).toEqualTypeOf<
+ CreateQueryResult, boolean>
+ >()
+ expectTypeOf(result1[0].data).toEqualTypeOf()
+ expectTypeOf(result1[1].data).toEqualTypeOf()
+ expectTypeOf(result1[2].data).toEqualTypeOf | undefined>()
+ expectTypeOf(result1[2].error).toEqualTypeOf()
+
+ // TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
+ const result2 = createQueries<
+ [
+ { queryFnData: string; data: string },
+ { queryFnData: string; data: number },
+ ]
+ >(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a.toLowerCase()
+ },
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return parseInt(a)
+ },
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result2[0]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result2[1]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result2[0].data).toEqualTypeOf()
+ expectTypeOf(result2[1].data).toEqualTypeOf()
+
+ // can pass only TData (data prop) although TQueryFnData will be left unknown
+ const result3 = createQueries<[{ data: string }, { data: number }]>(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a as string
+ },
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a as number
+ },
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result3[0]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result3[1]).toEqualTypeOf<
+ CreateQueryResult
+ >()
+ expectTypeOf(result3[0].data).toEqualTypeOf()
+ expectTypeOf(result3[1].data).toEqualTypeOf()
+
+ // types should be enforced
+ createQueries<
+ [
+ { queryFnData: string; data: string },
+ { queryFnData: string; data: number; error: boolean },
+ ]
+ >(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return a.toLowerCase()
+ },
+ placeholderData: 'string',
+ // @ts-expect-error (initialData: string)
+ initialData: 123,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ select: (a) => {
+ expectTypeOf(a).toEqualTypeOf()
+ return parseInt(a)
+ },
+ placeholderData: 'string',
+ // @ts-expect-error (initialData: string)
+ initialData: 123,
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ // field names should be enforced
+ createQueries<[{ queryFnData: string }]>(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+ }),
+ )
+
+ it(
+ 'handles array literal without type parameter to infer result type',
+ withEffectRoot(() => {
+ const key1 = ['test-key-1']
+ const key2 = ['test-key-2']
+ const key3 = ['test-key-3']
+ const key4 = ['test-key-4']
+
+ // Array.map preserves TQueryFnData
+ const result1 = createQueries(
+ () => ({
+ queries: Array(50).map((_, i) => ({
+ queryKey: ['key', i] as const,
+ queryFn: () => i + 10,
+ })),
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result1).toEqualTypeOf<
+ Array>
+ >()
+ if (result1[0]) {
+ expectTypeOf(result1[0].data).toEqualTypeOf()
+ }
+
+ // Array.map preserves TData
+ const result2 = createQueries(
+ () => ({
+ queries: Array(50).map((_, i) => ({
+ queryKey: ['key', i] as const,
+ queryFn: () => i + 10,
+ select: (data: number) => data.toString(),
+ })),
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result2).toEqualTypeOf<
+ Array>
+ >()
+
+ const result3 = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 1,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key3,
+ queryFn: () => ['string[]'],
+ select: () => 123,
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result3[0]).toEqualTypeOf>()
+ expectTypeOf(result3[1]).toEqualTypeOf>()
+ expectTypeOf(result3[2]).toEqualTypeOf>()
+ expectTypeOf(result3[0].data).toEqualTypeOf()
+ expectTypeOf(result3[1].data).toEqualTypeOf()
+ // select takes precedence over queryFn
+ expectTypeOf(result3[2].data).toEqualTypeOf()
+
+ // initialData/placeholderData are enforced
+ createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ placeholderData: 'string',
+ // @ts-expect-error (initialData: string)
+ initialData: 123,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 123,
+ // @ts-expect-error (placeholderData: number)
+ placeholderData: 'string',
+ initialData: 123,
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ // select params are "indirectly" enforced
+ createQueries(
+ () => ({
+ queries: [
+ // unfortunately TS will not suggest the type for you
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ },
+ // however you can add a type to the callback
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ },
+ // the type you do pass is enforced
+ {
+ queryKey: key3,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key4,
+ queryFn: () => 'string',
+ select: (a: string) => parseInt(a),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ // callbacks are also indirectly enforced with Array.map
+ createQueries(
+ () => ({
+ queries: Array(50).map((_, i) => ({
+ queryKey: ['key', i] as const,
+ queryFn: () => i + 10,
+ select: (data: number) => data.toString(),
+ })),
+ }),
+ () => queryClient,
+ )
+
+ // results inference works when all the handlers are defined
+ const result4 = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key2,
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: key4,
+ queryFn: () => 'string',
+ select: (a: string) => parseInt(a),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result4[0]).toEqualTypeOf>()
+ expectTypeOf(result4[1]).toEqualTypeOf>()
+ expectTypeOf(result4[2]).toEqualTypeOf>()
+
+ // handles when queryFn returns a Promise
+ const result5 = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => Promise.resolve('string'),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result5[0]).toEqualTypeOf>()
+
+ // Array as const does not throw error
+ const result6 = createQueries(
+ () =>
+ ({
+ queries: [
+ {
+ queryKey: ['key1'],
+ queryFn: () => 'string',
+ },
+ {
+ queryKey: ['key1'],
+ queryFn: () => 123,
+ },
+ ],
+ }) as const,
+ () => queryClient,
+ )
+
+ expectTypeOf(result6[0]).toEqualTypeOf>()
+ expectTypeOf(result6[1]).toEqualTypeOf>()
+
+ // field names should be enforced - array literal
+ createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => 'string',
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ // field names should be enforced - Array.map() result
+ createQueries(
+ () => ({
+ // @ts-expect-error (invalidField)
+ queries: Array(10).map(() => ({
+ someInvalidField: '',
+ })),
+ }),
+ () => queryClient,
+ )
+
+ // supports queryFn using fetch() to return Promise - Array.map() result
+ createQueries(
+ () => ({
+ queries: Array(50).map((_, i) => ({
+ queryKey: ['key', i] as const,
+ queryFn: () =>
+ fetch('return Promise').then((resp) => resp.json()),
+ })),
+ }),
+ () => queryClient,
+ )
+
+ // supports queryFn using fetch() to return Promise - array literal
+ createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () =>
+ fetch('return Promise').then((resp) => resp.json()),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+ }),
+ )
+
+ it(
+ 'handles strongly typed queryFn factories and createQueries wrappers',
+ withEffectRoot(() => {
+ // QueryKey + queryFn factory
+ type QueryKeyA = ['queryA']
+ const getQueryKeyA = (): QueryKeyA => ['queryA']
+ type GetQueryFunctionA = () => QueryFunction
+ const getQueryFunctionA: GetQueryFunctionA = () => () => {
+ return 1
+ }
+ type SelectorA = (data: number) => [number, string]
+ const getSelectorA = (): SelectorA => (data) => [data, data.toString()]
+
+ type QueryKeyB = ['queryB', string]
+ const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
+ type GetQueryFunctionB = () => QueryFunction
+ const getQueryFunctionB: GetQueryFunctionB = () => () => {
+ return '1'
+ }
+ type SelectorB = (data: string) => [string, number]
+ const getSelectorB = (): SelectorB => (data) => [data, +data]
+
+ // Wrapper with strongly typed array-parameter
+ function useWrappedQueries<
+ TQueryFnData,
+ TError,
+ TData,
+ TQueryKey extends QueryKey,
+ >(
+ queries: Array<
+ CreateQueryOptions
+ >,
+ ) {
+ return createQueries(
+ () => ({
+ queries: queries.map(
+ // no need to type the mapped query
+ (query) => {
+ const { queryFn: fn, queryKey: key } = query
+ expectTypeOf(fn).toEqualTypeOf<
+ | typeof skipToken
+ | QueryFunction
+ | undefined
+ >()
+ return {
+ queryKey: key,
+ queryFn: fn
+ ? (ctx: QueryFunctionContext) => {
+ // eslint-disable-next-line vitest/valid-expect
+ expectTypeOf(ctx.queryKey)
+ return (
+ fn as QueryFunction
+ ).call({}, ctx)
+ }
+ : undefined,
+ }
+ },
+ ),
+ }),
+ () => queryClient,
+ )
+ }
+
+ const result = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: getQueryKeyA(),
+ queryFn: getQueryFunctionA(),
+ },
+ {
+ queryKey: getQueryKeyB('id'),
+ queryFn: getQueryFunctionB(),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(result[0]).toEqualTypeOf>()
+ expectTypeOf(result[1]).toEqualTypeOf>()
+
+ const withSelector = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: getQueryKeyA(),
+ queryFn: getQueryFunctionA(),
+ select: getSelectorA(),
+ },
+ {
+ queryKey: getQueryKeyB('id'),
+ queryFn: getQueryFunctionB(),
+ select: getSelectorB(),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ expectTypeOf(withSelector[0]).toEqualTypeOf<
+ CreateQueryResult<[number, string], Error>
+ >()
+ expectTypeOf(withSelector[1]).toEqualTypeOf<
+ CreateQueryResult<[string, number], Error>
+ >()
+
+ const withWrappedQueries = useWrappedQueries(
+ Array(10).map(() => ({
+ queryKey: getQueryKeyA(),
+ queryFn: getQueryFunctionA(),
+ select: getSelectorA(),
+ })),
+ )
+
+ expectTypeOf(withWrappedQueries).toEqualTypeOf<
+ Array>
+ >()
+ }),
+ )
+
+ it(
+ 'should track results',
+ withEffectRoot(async () => {
+ const key1 = ['test-track-results']
+ const results: Array> = []
+ let count = 0
+
+ const result = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => Promise.resolve(++count),
+ },
+ ],
+ }),
+ () => queryClient,
+ )
+
+ $effect(() => {
+ results.push([result[0]])
+ })
+
+ await vi.waitFor(() => expect(result[0].data).toBe(1))
+
+ expect(results.length).toBe(2)
+ expect(results[0]).toMatchObject([{ data: undefined }])
+ expect(results[1]).toMatchObject([{ data: 1 }])
+
+ // Trigger refetch
+ result[0].refetch()
+
+ await vi.waitFor(() => expect(result[0].data).toBe(2))
+
+ // Only one render for data update, no render for isFetching transition
+ expect(results.length).toBe(3)
+ expect(results[2]).toMatchObject([{ data: 2 }])
+ }),
+ )
+
+ it(
+ 'should combine queries',
+ withEffectRoot(async () => {
+ const key1 = ['test-combine-1']
+ const key2 = ['test-combine-2']
+
+ const { promise: promise1, resolve: resolve1 } =
+ promiseWithResolvers()
+ const { promise: promise2, resolve: resolve2 } =
+ promiseWithResolvers()
+
+ const queries = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => promise1,
+ },
+ {
+ queryKey: key2,
+ queryFn: () => promise2,
+ },
+ ],
+ combine: (results) => {
+ return {
+ combined: true,
+ res: results
+ .flatMap((res) => (res.data ? [res.data] : []))
+ .join(','),
+ }
+ },
+ }),
+ () => queryClient,
+ )
+
+ // Initially both queries are loading
+ expect(queries).toEqual({
+ combined: true,
+ res: '',
+ })
+
+ // Resolve the first query
+ resolve1('first result')
+ await vi.waitFor(() => expect(queries.res).toBe('first result'))
+
+ // Resolve the second query
+ resolve2('second result')
+ await vi.waitFor(() =>
+ expect(queries.res).toBe('first result,second result'),
+ )
+
+ expect(queries).toEqual({
+ combined: true,
+ res: 'first result,second result',
+ })
+ }),
+ )
+
+ it(
+ 'should track property access through combine function',
+ withEffectRoot(async () => {
+ const key1 = ['test-track-combine-1']
+ const key2 = ['test-track-combine-2']
+ let count = 0
+ const results: Array = []
+
+ const { promise: promise1, resolve: resolve1 } =
+ promiseWithResolvers()
+ const { promise: promise2, resolve: resolve2 } =
+ promiseWithResolvers()
+ const { promise: promise3, resolve: resolve3 } =
+ promiseWithResolvers()
+ const { promise: promise4, resolve: resolve4 } =
+ promiseWithResolvers()
+
+ const queries = createQueries(
+ () => ({
+ queries: [
+ {
+ queryKey: key1,
+ queryFn: () => (count === 0 ? promise1 : promise3),
+ },
+ {
+ queryKey: key2,
+ queryFn: () => (count === 0 ? promise2 : promise4),
+ },
+ ],
+ combine: (queryResults) => {
+ return {
+ combined: true,
+ refetch: () =>
+ Promise.all(queryResults.map((res) => res.refetch())),
+ res: queryResults
+ .flatMap((res) => (res.data ? [res.data] : []))
+ .join(','),
+ }
+ },
+ }),
+ () => queryClient,
+ )
+
+ $effect(() => {
+ results.push({ ...queries })
+ })
+
+ // Initially both queries are loading
+ await vi.waitFor(() =>
+ expect(results[0]).toStrictEqual({
+ combined: true,
+ refetch: expect.any(Function),
+ res: '',
+ }),
+ )
+
+ // Resolve the first query
+ resolve1('first result ' + count)
+ await vi.waitFor(() => expect(queries.res).toBe('first result 0'))
+
+ expect(results[1]).toStrictEqual({
+ combined: true,
+ refetch: expect.any(Function),
+ res: 'first result 0',
+ })
+
+ // Resolve the second query
+ resolve2('second result ' + count)
+ await vi.waitFor(() =>
+ expect(queries.res).toBe('first result 0,second result 0'),
+ )
+
+ expect(results[2]).toStrictEqual({
+ combined: true,
+ refetch: expect.any(Function),
+ res: 'first result 0,second result 0',
+ })
+
+ // Increment count and refetch
+ count++
+ queries.refetch()
+
+ // Resolve the refetched queries
+ resolve3('first result ' + count)
+ resolve4('second result ' + count)
+
+ await vi.waitFor(() =>
+ expect(queries.res).toBe('first result 1,second result 1'),
+ )
+
+ const length = results.length
+ expect(results.at(-1)).toStrictEqual({
+ combined: true,
+ refetch: expect.any(Function),
+ res: 'first result 1,second result 1',
+ })
+
+ // Refetch again but with the same data
+ await queries.refetch()
+
+ // No further re-render because data didn't change
+ expect(results.length).toBe(length)
+ }),
+ )
+})
diff --git a/packages/svelte-query/tests/createQueries.test-d.ts b/packages/svelte-query/tests/createQueries.test-d.ts
new file mode 100644
index 0000000000..016f5a53a5
--- /dev/null
+++ b/packages/svelte-query/tests/createQueries.test-d.ts
@@ -0,0 +1,34 @@
+import { describe, expectTypeOf, it } from 'vitest'
+import { createQueries, queryOptions } from '../src/index.js'
+import type { CreateQueryResult } from '../src/index.js'
+
+describe('createQueries', () => {
+ it('should return correct data for dynamic queries with mixed result types', () => {
+ const Queries1 = {
+ get: () =>
+ queryOptions({
+ queryKey: ['key1'],
+ queryFn: () => Promise.resolve(1),
+ }),
+ }
+ const Queries2 = {
+ get: () =>
+ queryOptions({
+ queryKey: ['key2'],
+ queryFn: () => Promise.resolve(true),
+ }),
+ }
+
+ const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
+ const result = createQueries(() => ({
+ queries: [...queries1List, { ...Queries2.get() }],
+ }))
+
+ expectTypeOf(result).toEqualTypeOf<
+ [
+ ...Array>,
+ CreateQueryResult,
+ ]
+ >()
+ })
+})
diff --git a/packages/svelte-query/tests/createQueries/BaseExample.svelte b/packages/svelte-query/tests/createQueries/BaseExample.svelte
deleted file mode 100644
index 9dd218c8ab..0000000000
--- a/packages/svelte-query/tests/createQueries/BaseExample.svelte
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-{#each queries as query, index}
- Status {index + 1}: {query.status}
- Data {index + 1}: {query.data}
-{/each}
diff --git a/packages/svelte-query/tests/createQueries/CombineExample.svelte b/packages/svelte-query/tests/createQueries/CombineExample.svelte
deleted file mode 100644
index 4fb83f6c35..0000000000
--- a/packages/svelte-query/tests/createQueries/CombineExample.svelte
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-isPending: {queries.isPending}
-Data: {queries.data}
diff --git a/packages/svelte-query/tests/createQueries/createQueries.test-d.ts b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts
deleted file mode 100644
index 69cbe0d164..0000000000
--- a/packages/svelte-query/tests/createQueries/createQueries.test-d.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { describe, expectTypeOf, test } from 'vitest'
-import { skipToken } from '@tanstack/query-core'
-import { createQueries, queryOptions } from '../../src/index.js'
-import type { QueryObserverResult } from '@tanstack/query-core'
-import type { CreateQueryOptions } from '../../src/index.js'
-
-describe('createQueries', () => {
- test('TData should be defined when passed through queryOptions', () => {
- const options = queryOptions({
- queryKey: ['key'],
- queryFn: () => {
- return {
- wow: true,
- }
- },
- initialData: {
- wow: true,
- },
- })
- const queryResults = createQueries({ queries: () => [options] })
-
- const data = queryResults[0].data
-
- expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
- })
-
- test('Allow custom hooks using UseQueryOptions', () => {
- type Data = string
-
- const useCustomQueries = (options?: CreateQueryOptions) => {
- return createQueries({
- queries: () => [
- {
- ...options,
- queryKey: ['todos-key'],
- queryFn: () => Promise.resolve('data'),
- },
- ],
- })
- }
-
- const query = useCustomQueries()
- const data = query[0].data
-
- expectTypeOf(data).toEqualTypeOf()
- })
-
- test('TData should have correct type when conditional skipToken is passed', () => {
- const queryResults = createQueries({
- queries: () => [
- {
- queryKey: ['withSkipToken'],
- queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
- },
- ],
- })
-
- const firstResult = queryResults[0]
-
- expectTypeOf(firstResult).toEqualTypeOf<
- QueryObserverResult
- >()
- expectTypeOf(firstResult.data).toEqualTypeOf()
- })
-
- test('should return correct data for dynamic queries with mixed result types', () => {
- const Queries1 = {
- get: () =>
- queryOptions({
- queryKey: ['key1'],
- queryFn: () => Promise.resolve(1),
- }),
- }
- const Queries2 = {
- get: () =>
- queryOptions({
- queryKey: ['key2'],
- queryFn: () => Promise.resolve(true),
- }),
- }
-
- const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
- const result = createQueries({
- queries: () => [...queries1List, { ...Queries2.get() }],
- })
-
- expectTypeOf(result).toEqualTypeOf<
- [
- ...Array>,
- QueryObserverResult,
- ]
- >()
-
- expectTypeOf(result[0].data).toEqualTypeOf()
- })
-})
diff --git a/packages/svelte-query/tests/createQueries/createQueries.test.ts b/packages/svelte-query/tests/createQueries/createQueries.test.ts
deleted file mode 100644
index bd0c098e70..0000000000
--- a/packages/svelte-query/tests/createQueries/createQueries.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { describe, expect, test } from 'vitest'
-import { render, waitFor } from '@testing-library/svelte'
-import { QueryClient } from '@tanstack/query-core'
-import { sleep } from '../utils.svelte.js'
-import BaseExample from './BaseExample.svelte'
-import CombineExample from './CombineExample.svelte'
-
-describe('createQueries', () => {
- test('Render and wait for success', async () => {
- const rendered = render(BaseExample, {
- props: {
- options: {
- queries: () => [
- {
- queryKey: ['key-1'],
- queryFn: async () => {
- await sleep(5)
- return 'Success 1'
- },
- },
- {
- queryKey: ['key-2'],
- queryFn: async () => {
- await sleep(5)
- return 'Success 2'
- },
- },
- ],
- },
- queryClient: new QueryClient(),
- },
- })
-
- await waitFor(() => {
- expect(rendered.getByText('Status 1: pending')).toBeInTheDocument()
- expect(rendered.getByText('Status 2: pending')).toBeInTheDocument()
- })
-
- await waitFor(() => {
- expect(rendered.getByText('Status 1: success')).toBeInTheDocument()
- expect(rendered.getByText('Status 2: success')).toBeInTheDocument()
- })
- })
-
- test('Combine queries', async () => {
- const rendered = render(CombineExample, {
- props: {
- queryClient: new QueryClient(),
- },
- })
-
- await waitFor(() => {
- expect(rendered.getByText('isPending: true')).toBeInTheDocument()
- })
-
- await waitFor(() => {
- expect(rendered.getByText('Data: 1,2,3')).toBeInTheDocument()
- })
- })
-})
diff --git a/packages/svelte-query/tests/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery.svelte.test.ts
new file mode 100644
index 0000000000..e5c52e0881
--- /dev/null
+++ b/packages/svelte-query/tests/createQuery.svelte.test.ts
@@ -0,0 +1,1894 @@
+import { flushSync } from 'svelte'
+import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
+import {
+ QueryCache,
+ QueryClient,
+ createQuery,
+ keepPreviousData,
+} from '../src/index.js'
+import { promiseWithResolvers, sleep, withEffectRoot } from './utils.svelte.js'
+import type { CreateQueryResult } from '../src/index.js'
+
+describe('createQuery', () => {
+ const queryCache = new QueryCache()
+ const queryClient = new QueryClient({ queryCache })
+
+ beforeEach(() => {
+ queryCache.clear()
+ })
+
+ it(
+ 'should return the correct states for a successful query',
+ withEffectRoot(async () => {
+ const { promise, resolve } = promiseWithResolvers()
+
+ const query = createQuery(
+ () => ({
+ queryKey: ['test'],
+ queryFn: () => promise,
+ }),
+ () => queryClient,
+ )
+
+ if (query.isPending) {
+ expectTypeOf(query.data).toEqualTypeOf()
+ expectTypeOf(query.error).toEqualTypeOf()
+ } else if (query.isLoadingError) {
+ expectTypeOf(query.data).toEqualTypeOf()
+ expectTypeOf(query.error).toEqualTypeOf()
+ } else {
+ expectTypeOf(query.data).toEqualTypeOf()
+ expectTypeOf(query.error).toEqualTypeOf()
+ }
+
+ const promise1 = query.promise
+
+ expect(query).toEqual({
+ data: undefined,
+ dataUpdatedAt: 0,
+ error: null,
+ errorUpdatedAt: 0,
+ failureCount: 0,
+ failureReason: null,
+ errorUpdateCount: 0,
+ isError: false,
+ isFetched: false,
+ isFetchedAfterMount: false,
+ isFetching: true,
+ isPaused: false,
+ isPending: true,
+ isInitialLoading: true,
+ isLoading: true,
+ isLoadingError: false,
+ isPlaceholderData: false,
+ isRefetchError: false,
+ isRefetching: false,
+ isStale: true,
+ isSuccess: false,
+ refetch: expect.any(Function),
+ status: 'pending',
+ fetchStatus: 'fetching',
+ promise: expect.any(Promise),
+ })
+ resolve('resolved')
+ await vi.waitFor(() =>
+ expect(query).toEqual({
+ data: 'resolved',
+ dataUpdatedAt: expect.any(Number),
+ error: null,
+ errorUpdatedAt: 0,
+ failureCount: 0,
+ failureReason: null,
+ errorUpdateCount: 0,
+ isError: false,
+ isFetched: true,
+ isFetchedAfterMount: true,
+ isFetching: false,
+ isPaused: false,
+ isPending: false,
+ isInitialLoading: false,
+ isLoading: false,
+ isLoadingError: false,
+ isPlaceholderData: false,
+ isRefetchError: false,
+ isRefetching: false,
+ isStale: true,
+ isSuccess: true,
+ refetch: expect.any(Function),
+ status: 'success',
+ fetchStatus: 'idle',
+ promise: expect.any(Promise),
+ }),
+ )
+
+ expect(promise1).toBe(query.promise)
+ }),
+ )
+
+ it(
+ 'should return the correct states for an unsuccessful query',
+ withEffectRoot(async () => {
+ let count = 0
+ const states: Array = []
+ const query = createQuery