@tanstack/solid-query 6.0.0-beta.3 → 6.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { DefaultError, WithRequired } from '@tanstack/query-core'
2
- import type { SolidMutationOptions } from './types'
2
+ import type { MutationOptions } from './types'
3
3
 
4
4
  export function mutationOptions<
5
5
  TData = unknown,
@@ -8,11 +8,11 @@ export function mutationOptions<
8
8
  TOnMutateResult = unknown,
9
9
  >(
10
10
  options: WithRequired<
11
- SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>,
11
+ MutationOptions<TData, TError, TVariables, TOnMutateResult>,
12
12
  'mutationKey'
13
13
  >,
14
14
  ): WithRequired<
15
- SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>,
15
+ MutationOptions<TData, TError, TVariables, TOnMutateResult>,
16
16
  'mutationKey'
17
17
  >
18
18
  export function mutationOptions<
@@ -22,11 +22,11 @@ export function mutationOptions<
22
22
  TOnMutateResult = unknown,
23
23
  >(
24
24
  options: Omit<
25
- SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>,
25
+ MutationOptions<TData, TError, TVariables, TOnMutateResult>,
26
26
  'mutationKey'
27
27
  >,
28
28
  ): Omit<
29
- SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>,
29
+ MutationOptions<TData, TError, TVariables, TOnMutateResult>,
30
30
  'mutationKey'
31
31
  >
32
32
  export function mutationOptions<
@@ -35,7 +35,7 @@ export function mutationOptions<
35
35
  TVariables = void,
36
36
  TOnMutateResult = unknown,
37
37
  >(
38
- options: SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>,
39
- ): SolidMutationOptions<TData, TError, TVariables, TOnMutateResult> {
38
+ options: MutationOptions<TData, TError, TVariables, TOnMutateResult>,
39
+ ): MutationOptions<TData, TError, TVariables, TOnMutateResult> {
40
40
  return options
41
41
  }
@@ -1,5 +1,5 @@
1
1
  import type { DataTag, DefaultError, QueryKey } from '@tanstack/query-core'
2
- import type { SolidQueryOptions } from './types'
2
+ import type { QueryOptions } from './types'
3
3
  import type { Accessor } from 'solid-js'
4
4
 
5
5
  export type UndefinedInitialDataOptions<
@@ -8,7 +8,7 @@ export type UndefinedInitialDataOptions<
8
8
  TData = TQueryFnData,
9
9
  TQueryKey extends QueryKey = QueryKey,
10
10
  > = Accessor<
11
- SolidQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
11
+ QueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
12
12
  initialData?: undefined
13
13
  }
14
14
  >
@@ -19,7 +19,7 @@ export type DefinedInitialDataOptions<
19
19
  TData = TQueryFnData,
20
20
  TQueryKey extends QueryKey = QueryKey,
21
21
  > = Accessor<
22
- SolidQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
22
+ QueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
23
23
  initialData: TQueryFnData | (() => TQueryFnData)
24
24
  }
25
25
  >
package/src/types.ts CHANGED
@@ -44,7 +44,7 @@ export interface UseBaseQueryOptions<
44
44
  suspense?: boolean
45
45
  }
46
46
 
47
- export interface SolidQueryOptions<
47
+ export interface QueryOptions<
48
48
  TQueryFnData = unknown,
49
49
  TError = DefaultError,
50
50
  TData = TQueryFnData,
@@ -62,7 +62,7 @@ export type UseQueryOptions<
62
62
  TError = DefaultError,
63
63
  TData = TQueryFnData,
64
64
  TQueryKey extends QueryKey = QueryKey,
65
- > = Accessor<SolidQueryOptions<TQueryFnData, TError, TData, TQueryKey>>
65
+ > = Accessor<QueryOptions<TQueryFnData, TError, TData, TQueryKey>>
66
66
 
67
67
  /* --- Create Query and Create Base Query Types --- */
68
68
 
@@ -87,7 +87,7 @@ export type DefinedUseQueryResult<
87
87
  > = DefinedUseBaseQueryResult<TData, TError>
88
88
 
89
89
  /* --- Create Infinite Queries Types --- */
90
- export interface SolidInfiniteQueryOptions<
90
+ export interface InfiniteQueryOptions<
91
91
  TQueryFnData = unknown,
92
92
  TError = DefaultError,
93
93
  TData = TQueryFnData,
@@ -126,7 +126,7 @@ export type UseInfiniteQueryOptions<
126
126
  TQueryKey extends QueryKey = QueryKey,
127
127
  TPageParam = unknown,
128
128
  > = Accessor<
129
- SolidInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
129
+ InfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>
130
130
  >
131
131
 
132
132
  export type UseInfiniteQueryResult<
@@ -140,7 +140,7 @@ export type DefinedUseInfiniteQueryResult<
140
140
  > = DefinedInfiniteQueryObserverResult<TData, TError>
141
141
 
142
142
  /* --- Create Mutation Types --- */
143
- export interface SolidMutationOptions<
143
+ export interface MutationOptions<
144
144
  TData = unknown,
145
145
  TError = DefaultError,
146
146
  TVariables = void,
@@ -155,7 +155,7 @@ export type UseMutationOptions<
155
155
  TError = DefaultError,
156
156
  TVariables = void,
157
157
  TOnMutateResult = unknown,
158
- > = Accessor<SolidMutationOptions<TData, TError, TVariables, TOnMutateResult>>
158
+ > = Accessor<MutationOptions<TData, TError, TVariables, TOnMutateResult>>
159
159
 
160
160
  export type UseMutateFunction<
161
161
  TData = unknown,
@@ -3,13 +3,14 @@
3
3
  // why that happens.
4
4
  import { notifyManager, shouldThrowError } from '@tanstack/query-core'
5
5
  import {
6
- createMemo,
7
6
  createRenderEffect,
7
+ createSignal,
8
8
  createStore,
9
9
  isPending,
10
10
  onCleanup,
11
11
  reconcile,
12
12
  refresh,
13
+ runWithOwner,
13
14
  snapshot,
14
15
  untrack,
15
16
  } from 'solid-js'
@@ -151,7 +152,12 @@ export function useBaseQuery<
151
152
  ) {
152
153
  type ResourceData = QueryObserverResult<TData, TError>
153
154
 
154
- const client = createMemo(() => useQueryClient(queryClient?.()))
155
+ // Use createSignal(fn) instead of createMemo so these derived memos have
156
+ // _preventAutoDisposal set. Without it, a createMemo that no one reads
157
+ // reactively gets auto-disposed in Solid v2, which cascades and disposes
158
+ // the component's onCleanup, unsubscribing the observer before the fetch
159
+ // completes.
160
+ const [client] = createSignal(() => useQueryClient(queryClient?.()))
155
161
  const isRestoring = useIsRestoring()
156
162
  // There are times when we run a query on the server but the resource is never read
157
163
  // This could lead to times when the queryObserver is unsubscribed before the resource has loaded
@@ -159,7 +165,7 @@ export function useBaseQuery<
159
165
  // before the resource has loaded
160
166
  let unsubscribeQueued = false
161
167
 
162
- const defaultedOptions = createMemo(() => {
168
+ const [defaultedOptions] = createSignal(() => {
163
169
  const defaultOptions = client().defaultQueryOptions(options())
164
170
  defaultOptions._optimisticResults = isRestoring()
165
171
  ? 'isRestoring'
@@ -178,7 +184,7 @@ export function useBaseQuery<
178
184
  const observer = untrack(() => new Observer(client(), defaultedOptions()))
179
185
 
180
186
  // Track options reactively so the queryResource memo re-runs on change.
181
- const trackedDefaultedOptions = createMemo(() => defaultedOptions())
187
+ const [trackedDefaultedOptions] = createSignal(() => defaultedOptions())
182
188
 
183
189
  // Apply options in an effect to avoid store writes inside the memo.
184
190
  // setOptions triggers updateResult → notify → subscription → setState,
@@ -186,7 +192,11 @@ export function useBaseQuery<
186
192
  createRenderEffect(
187
193
  () => trackedDefaultedOptions(),
188
194
  (opts) => {
189
- observer.setOptions(opts)
195
+ // observer.setOptions synchronously invokes subscribers which write to
196
+ // the store. In Solid v2, signal/store writes inside an owned scope
197
+ // (like this render effect) throw. Escape the owner so subscriber
198
+ // writes don't trip the guard.
199
+ runWithOwner(null, () => observer.setOptions(opts))
190
200
  },
191
201
  )
192
202
 
@@ -231,8 +241,8 @@ export function useBaseQuery<
231
241
  return observer.subscribe((result) => {
232
242
  const previousResult = observerResult
233
243
  observerResult = result
234
- setStateWithReconciliation(result)
235
- queueMicrotask(() => {
244
+ runWithOwner(null, () => {
245
+ setStateWithReconciliation(result)
236
246
  if (
237
247
  unsubscribe &&
238
248
  !disposed &&
@@ -243,8 +253,7 @@ export function useBaseQuery<
243
253
  refresh(queryResource)
244
254
  } catch {
245
255
  // NotReadyError is expected when refreshing a memo that returns
246
- // a Promise. The Loading boundary handles this during rendering,
247
- // but when refresh is called from a microtask there is no boundary.
256
+ // a Promise. The Loading boundary handles this during rendering.
248
257
  }
249
258
  }
250
259
  })
@@ -280,27 +289,59 @@ export function useBaseQuery<
280
289
  but the resource is still in a loading state
281
290
  */
282
291
  let resolver: ((value: ResourceData) => void) | null = null
283
- const queryResource = createMemo<ResourceData>(
284
- () => {
285
- // Read trackedDefaultedOptions to ensure this memo re-runs when options change
286
- const opts = trackedDefaultedOptions()
287
- // Read isRestoring unconditionally so the memo re-runs when it changes
288
- const restoring = isRestoring()
289
-
290
- return new Promise((resolve, reject) => {
291
- resolver = resolve
292
- if (isServer) {
293
- unsubscribe = createServerSubscriber((data) => {
294
- resolve(data as ResourceData)
295
- }, reject)
296
- } else if (!unsubscribe && !restoring) {
297
- unsubscribe = createClientSubscriber()
298
- }
299
- // Use getOptimisticResult instead of updateResult to keep the memo
300
- // free of store writes (updateResult triggers notify → setState).
301
- const currentResult = observer.getOptimisticResult(opts)
302
- observerResult = currentResult
292
+ // Use createSignal(fn) instead of createMemo so the derived memo has
293
+ // _preventAutoDisposal set. Without it, a createMemo that no one reads
294
+ // reactively gets auto-disposed in Solid v2, which would cascade-dispose
295
+ // the component's onCleanup and unsubscribe the observer before fetch
296
+ // completion.
297
+ const [queryResource] = createSignal<ResourceData>(() => {
298
+ // Read trackedDefaultedOptions to ensure this memo re-runs when options change
299
+ const opts = trackedDefaultedOptions()
300
+ // Read isRestoring unconditionally so the memo re-runs when it changes
301
+ const restoring = isRestoring()
303
302
 
303
+ if (isServer) {
304
+ // On retry passes (after the streaming Loading boundary awaits a
305
+ // pending Promise), the QueryClient cache already has the data, so
306
+ // `getOptimisticResult` returns a non-loading result synchronously.
307
+ // Returning the value directly (instead of a fresh Promise that
308
+ // resolves synchronously) lets Solid's `processResult` set
309
+ // `comp.value` directly without queueing another async settle. If we
310
+ // returned a new Promise on every retry, Solid's streaming Loading
311
+ // boundary `while (ret.p.length)` loop in `createLoadingBoundary`
312
+ // would never terminate, because each retry adds a new pending
313
+ // Promise to the boundary's tracked set even when it resolves
314
+ // synchronously.
315
+ const cached = observer.getOptimisticResult(opts)
316
+ if (!cached.isLoading) {
317
+ observerResult = cached
318
+ runWithOwner(null, () => {
319
+ setStateWithReconciliation(cached)
320
+ })
321
+ return hydratableObserverResult(
322
+ observer.getCurrentQuery(),
323
+ cached,
324
+ ) as ResourceData
325
+ }
326
+ }
327
+
328
+ return new Promise<ResourceData>((resolve, reject) => {
329
+ resolver = resolve
330
+ if (isServer) {
331
+ unsubscribe = createServerSubscriber((data) => {
332
+ resolve(data as ResourceData)
333
+ }, reject)
334
+ } else if (!unsubscribe && !restoring) {
335
+ unsubscribe = createClientSubscriber()
336
+ }
337
+ // Use getOptimisticResult instead of updateResult to keep the memo
338
+ // free of store writes (updateResult triggers notify → setState).
339
+ const currentResult = observer.getOptimisticResult(opts)
340
+ observerResult = currentResult
341
+
342
+ // Store writes inside a memo's owned scope throw in Solid v2.
343
+ // Escape the owner so setState calls are allowed.
344
+ runWithOwner(null, () => {
304
345
  if (
305
346
  currentResult.isError &&
306
347
  !currentResult.isFetching &&
@@ -311,23 +352,31 @@ export function useBaseQuery<
311
352
  ])
312
353
  ) {
313
354
  setStateWithReconciliation(currentResult)
314
- return reject(currentResult.error)
355
+ reject(currentResult.error)
356
+ return
315
357
  }
316
- if (!currentResult.isLoading) {
317
- resolver = null
318
- setStateWithReconciliation(currentResult)
319
- return resolve(
320
- hydratableObserverResult(observer.getCurrentQuery(), currentResult),
321
- )
322
- }
323
-
324
- // Defer the loading-state store write so it runs outside the memo.
325
- queueMicrotask(() => setStateWithReconciliation(currentResult))
358
+ setStateWithReconciliation(currentResult)
326
359
  })
327
- },
328
- observerResult,
329
- { ssrSource: 'client' },
330
- )
360
+
361
+ if (
362
+ currentResult.isError &&
363
+ !currentResult.isFetching &&
364
+ !restoring &&
365
+ shouldThrowError(opts.throwOnError, [
366
+ currentResult.error,
367
+ observer.getCurrentQuery(),
368
+ ])
369
+ ) {
370
+ return
371
+ }
372
+ if (!currentResult.isLoading) {
373
+ resolver = null
374
+ return resolve(
375
+ hydratableObserverResult(observer.getCurrentQuery(), currentResult),
376
+ )
377
+ }
378
+ })
379
+ })
331
380
 
332
381
  onCleanup(() => {
333
382
  disposed = true
@@ -364,6 +413,26 @@ export function useBaseQuery<
364
413
  return Reflect.get(target, prop, receiver)
365
414
  }
366
415
 
416
+ // On the server, force a Suspense dependency on the query resource so
417
+ // the per-route Loading boundary catches NotReadyError, awaits the
418
+ // pending Promise, and re-renders with the resolved state. Without
419
+ // this, JSX reads through the Proxy never subscribe to queryResource
420
+ // and SSR HTML reflects the initial loading state.
421
+ //
422
+ // Read the value from the *resolved resource* rather than `state`. When
423
+ // the boundary suspends and re-renders after the query settles, the
424
+ // `state` store is not synced (the server subscriber resolves the
425
+ // resource Promise but does not write the store), so reading `state`
426
+ // would render stale loading values. Reading the resolved resource keeps
427
+ // the streamed SSR HTML consistent with the serialized resource, which
428
+ // is what the client hydrates against.
429
+ if (isServer) {
430
+ const resolved = queryResource()
431
+ if (prop in resolved) {
432
+ return Reflect.get(resolved, prop)
433
+ }
434
+ }
435
+
367
436
  // Always pass through error-related props without throwing
368
437
  if (errorPassthroughProps.has(prop)) {
369
438
  return Reflect.get(target, prop, receiver)
@@ -13,7 +13,7 @@ export function useIsFetching(
13
13
 
14
14
  const [fetches, setFetches] = createSignal(
15
15
  untrack(() => client().isFetching(filters?.())),
16
- { pureWrite: true },
16
+ { ownedWrite: true },
17
17
  )
18
18
 
19
19
  const unsubscribe = untrack(() =>
@@ -13,7 +13,7 @@ export function useIsMutating(
13
13
 
14
14
  const [mutations, setMutations] = createSignal(
15
15
  untrack(() => client().isMutating(filters?.())),
16
- { pureWrite: true },
16
+ { ownedWrite: true },
17
17
  )
18
18
 
19
19
  const unsubscribe = untrack(() =>
@@ -4,6 +4,7 @@ import {
4
4
  createRenderEffect,
5
5
  createStore,
6
6
  onCleanup,
7
+ runWithOwner,
7
8
  untrack,
8
9
  } from 'solid-js'
9
10
  import { useQueryClient } from './QueryClientProvider'
@@ -63,11 +64,17 @@ export function useMutation<
63
64
  })
64
65
 
65
66
  const unsubscribe = observer.subscribe((result) => {
66
- setState(() => ({
67
- ...result,
68
- mutate,
69
- mutateAsync: result.mutate,
70
- }))
67
+ // observer.mutate may be invoked synchronously from an owned scope (e.g.
68
+ // during render or inside an effect), which triggers this subscriber
69
+ // synchronously. Store writes inside an owned scope throw in Solid v2, so
70
+ // escape the owner to allow the setState write — matching useBaseQuery.
71
+ runWithOwner(null, () => {
72
+ setState(() => ({
73
+ ...result,
74
+ mutate,
75
+ mutateAsync: result.mutate,
76
+ }))
77
+ })
71
78
  })
72
79
 
73
80
  onCleanup(unsubscribe)
@@ -36,7 +36,7 @@ export function useMutationState<TResult = MutationState>(
36
36
 
37
37
  const [result, setResult] = createSignal(
38
38
  untrack(() => getResult(mutationCache(), options())),
39
- { pureWrite: true },
39
+ { ownedWrite: true },
40
40
  )
41
41
 
42
42
  const unsubscribe = untrack(() =>
package/src/useQueries.ts CHANGED
@@ -6,11 +6,12 @@ import {
6
6
  merge,
7
7
  onCleanup,
8
8
  reconcile,
9
+ runWithOwner,
9
10
  untrack,
10
11
  } from 'solid-js'
11
12
  import { useQueryClient } from './QueryClientProvider'
12
13
  import { useIsRestoring } from './isRestoring'
13
- import type { SolidQueryOptions, UseQueryResult } from './types'
14
+ import type { QueryOptions, UseQueryResult } from './types'
14
15
  import type { Accessor } from 'solid-js'
15
16
  import type { QueryClient } from './QueryClient'
16
17
  import type {
@@ -33,7 +34,7 @@ type UseQueryOptionsForUseQueries<
33
34
  TData = TQueryFnData,
34
35
  TQueryKey extends QueryKey = QueryKey,
35
36
  > = OmitKeyof<
36
- SolidQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
37
+ QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
37
38
  'placeholderData' | 'suspense'
38
39
  > & {
39
40
  placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
@@ -244,14 +245,16 @@ export function useQueries<
244
245
  () => {
245
246
  if (!isRestoring()) {
246
247
  unsubscribe = observer.subscribe((result) => {
247
- setState(
248
- reconcile(
249
- [...result] as Array<QueryObserverResult>,
250
- // Use a key function that returns undefined so reconcile
251
- // uses positional matching and recursively updates nested properties
252
- () => undefined,
253
- ),
254
- )
248
+ runWithOwner(null, () => {
249
+ setState(
250
+ reconcile(
251
+ [...result] as Array<QueryObserverResult>,
252
+ // Use a key function that returns undefined so reconcile
253
+ // uses positional matching and recursively updates nested properties
254
+ () => undefined,
255
+ ),
256
+ )
257
+ })
255
258
  })
256
259
  }
257
260
  },
@@ -265,14 +268,16 @@ export function useQueries<
265
268
  // Update observer queries when options change reactively
266
269
  const trackedDefaultedQueries = createMemo(() => {
267
270
  const queries = defaultedQueries()
268
- observer.setQueries(
269
- queries,
270
- queriesOptions().combine
271
- ? ({
272
- combine: queriesOptions().combine,
273
- } as QueriesObserverOptions<TCombinedResult>)
274
- : undefined,
275
- )
271
+ runWithOwner(null, () => {
272
+ observer.setQueries(
273
+ queries,
274
+ queriesOptions().combine
275
+ ? ({
276
+ combine: queriesOptions().combine,
277
+ } as QueriesObserverOptions<TCombinedResult>)
278
+ : undefined,
279
+ )
280
+ })
276
281
  return queries
277
282
  })
278
283