@tanstack/solid-query 5.0.0-beta.9 → 5.0.0-rc.0

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.
@@ -28,7 +28,7 @@ describe("useQuery's in Suspense mode", () => {
28
28
 
29
29
  it('should render the correct amount of times in Suspense mode', async () => {
30
30
  const key = queryKey()
31
- const states: CreateQueryResult<number>[] = []
31
+ const states: Array<CreateQueryResult<number>> = []
32
32
 
33
33
  let count = 0
34
34
  let renders = 0
@@ -84,7 +84,7 @@ describe("useQuery's in Suspense mode", () => {
84
84
 
85
85
  it('should return the correct states for a successful infinite query', async () => {
86
86
  const key = queryKey()
87
- const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
87
+ const states: Array<CreateInfiniteQueryResult<InfiniteData<number>>> = []
88
88
 
89
89
  function Page() {
90
90
  const [multiplier, setMultiplier] = createSignal(1)
@@ -94,7 +94,7 @@ describe("useQuery's in Suspense mode", () => {
94
94
  await sleep(10)
95
95
  return Number(pageParam * multiplier())
96
96
  },
97
- defaultPageParam: 1,
97
+ initialPageParam: 1,
98
98
  suspense: true,
99
99
  getNextPageParam: (lastPage) => lastPage + 1,
100
100
  }))
@@ -143,7 +143,7 @@ describe("useQuery's in Suspense mode", () => {
143
143
  it('should not call the queryFn twice when used in Suspense mode', async () => {
144
144
  const key = queryKey()
145
145
 
146
- const queryFn = vi.fn<unknown[], string>()
146
+ const queryFn = vi.fn<Array<unknown>, string>()
147
147
  queryFn.mockImplementation(() => {
148
148
  sleep(10)
149
149
  return 'data'
@@ -453,6 +453,10 @@ describe("useQuery's in Suspense mode", () => {
453
453
  it('should throw errors to the error boundary by default', async () => {
454
454
  const key = queryKey()
455
455
 
456
+ const consoleMock = vi
457
+ .spyOn(console, 'error')
458
+ .mockImplementation(() => undefined)
459
+
456
460
  function Page() {
457
461
  const state = createQuery(() => ({
458
462
  queryKey: key,
@@ -497,6 +501,8 @@ describe("useQuery's in Suspense mode", () => {
497
501
 
498
502
  await waitFor(() => screen.getByText('Loading...'))
499
503
  await waitFor(() => screen.getByText('error boundary'))
504
+
505
+ consoleMock.mockRestore()
500
506
  })
501
507
 
502
508
  it('should not throw errors to the error boundary when throwOnError: false', async () => {
@@ -551,6 +557,10 @@ describe("useQuery's in Suspense mode", () => {
551
557
  it('should throw errors to the error boundary when a throwOnError function returns true', async () => {
552
558
  const key = queryKey()
553
559
 
560
+ const consoleMock = vi
561
+ .spyOn(console, 'error')
562
+ .mockImplementation(() => undefined)
563
+
554
564
  function Page() {
555
565
  const state = createQuery(() => ({
556
566
  queryKey: key,
@@ -595,6 +605,8 @@ describe("useQuery's in Suspense mode", () => {
595
605
 
596
606
  await waitFor(() => screen.getByText('Loading...'))
597
607
  await waitFor(() => screen.getByText('error boundary'))
608
+
609
+ consoleMock.mockRestore()
598
610
  })
599
611
 
600
612
  it('should not throw errors to the error boundary when a throwOnError function returns false', async () => {
@@ -651,7 +663,7 @@ describe("useQuery's in Suspense mode", () => {
651
663
  it('should not call the queryFn when not enabled', async () => {
652
664
  const key = queryKey()
653
665
 
654
- const queryFn = vi.fn<unknown[], Promise<string>>()
666
+ const queryFn = vi.fn<Array<unknown>, Promise<string>>()
655
667
  queryFn.mockImplementation(async () => {
656
668
  await sleep(10)
657
669
  return '23'
@@ -696,6 +708,10 @@ describe("useQuery's in Suspense mode", () => {
696
708
  it('should error catched in error boundary without infinite loop', async () => {
697
709
  const key = queryKey()
698
710
 
711
+ const consoleMock = vi
712
+ .spyOn(console, 'error')
713
+ .mockImplementation(() => undefined)
714
+
699
715
  let succeed = true
700
716
 
701
717
  function Page() {
@@ -756,11 +772,17 @@ describe("useQuery's in Suspense mode", () => {
756
772
  fireEvent.click(screen.getByLabelText('fail'))
757
773
  // render error boundary fallback (error boundary)
758
774
  await waitFor(() => screen.getByText('error boundary'))
775
+
776
+ consoleMock.mockRestore()
759
777
  })
760
778
 
761
779
  it('should error catched in error boundary without infinite loop when query keys changed', async () => {
762
780
  let succeed = true
763
781
 
782
+ const consoleMock = vi
783
+ .spyOn(console, 'error')
784
+ .mockImplementation(() => undefined)
785
+
764
786
  function Page() {
765
787
  const [key, setKey] = createSignal(0)
766
788
 
@@ -814,9 +836,15 @@ describe("useQuery's in Suspense mode", () => {
814
836
  fireEvent.click(screen.getByLabelText('fail'))
815
837
  // render error boundary fallback (error boundary)
816
838
  await waitFor(() => screen.getByText('error boundary'))
839
+
840
+ consoleMock.mockRestore()
817
841
  })
818
842
 
819
843
  it('should error catched in error boundary without infinite loop when enabled changed', async () => {
844
+ const consoleMock = vi
845
+ .spyOn(console, 'error')
846
+ .mockImplementation(() => undefined)
847
+
820
848
  function Page() {
821
849
  const queryKeys = '1'
822
850
  const [enabled, setEnabled] = createSignal(false)
@@ -874,6 +902,8 @@ describe("useQuery's in Suspense mode", () => {
874
902
 
875
903
  // render error boundary fallback (error boundary)
876
904
  await waitFor(() => screen.getByText('error boundary'))
905
+
906
+ consoleMock.mockRestore()
877
907
  })
878
908
 
879
909
  it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {
@@ -4,7 +4,7 @@ import { Show, Suspense, createSignal, startTransition } from 'solid-js'
4
4
  import { QueryCache, QueryClientProvider, createQuery } from '..'
5
5
  import { createQueryClient, queryKey, sleep } from './utils'
6
6
 
7
- describe("useQuery's in Suspense mode with transitions", () => {
7
+ describe("createQuery's in Suspense mode with transitions", () => {
8
8
  const queryCache = new QueryCache()
9
9
  const queryClient = createQueryClient({ queryCache })
10
10
 
@@ -19,7 +19,6 @@ describe("useQuery's in Suspense mode with transitions", () => {
19
19
  return true
20
20
  },
21
21
  }))
22
-
23
22
  return <Show when={state.data}>Message</Show>
24
23
  }
25
24
 
@@ -59,7 +59,7 @@ describe('useIsFetching', () => {
59
59
  const key1 = queryKey()
60
60
  const key2 = queryKey()
61
61
 
62
- const isFetchings: number[] = []
62
+ const isFetchings: Array<number> = []
63
63
 
64
64
  function IsFetching() {
65
65
  const isFetching = useIsFetching()
@@ -125,7 +125,7 @@ describe('useIsFetching', () => {
125
125
  const key1 = queryKey()
126
126
  const key2 = queryKey()
127
127
 
128
- const isFetchings: number[] = []
128
+ const isFetchings: Array<number> = []
129
129
 
130
130
  function One() {
131
131
  createQuery(() => ({
@@ -8,7 +8,7 @@ import { createQueryClient, setActTimeout, sleep } from './utils'
8
8
 
9
9
  describe('useIsMutating', () => {
10
10
  it('should return the number of fetching mutations', async () => {
11
- const isMutatings: number[] = []
11
+ const isMutatings: Array<number> = []
12
12
  const queryClient = createQueryClient()
13
13
 
14
14
  function IsMutating() {
@@ -63,7 +63,7 @@ describe('useIsMutating', () => {
63
63
  })
64
64
 
65
65
  it('should filter correctly by mutationKey', async () => {
66
- const isMutatings: number[] = []
66
+ const isMutatings: Array<number> = []
67
67
  const queryClient = createQueryClient()
68
68
 
69
69
  function IsMutating() {
@@ -108,7 +108,7 @@ describe('useIsMutating', () => {
108
108
  })
109
109
 
110
110
  it('should filter correctly by predicate', async () => {
111
- const isMutatings: number[] = []
111
+ const isMutatings: Array<number> = []
112
112
  const queryClient = createQueryClient()
113
113
 
114
114
  function IsMutating() {
@@ -7,7 +7,7 @@ import type { ParentProps } from 'solid-js'
7
7
  import type { SpyInstance } from 'vitest'
8
8
 
9
9
  let queryKeyCount = 0
10
- export function queryKey(): Array<string> {
10
+ export function queryKey() {
11
11
  queryKeyCount++
12
12
  return [`query_${queryKeyCount}`]
13
13
  }
@@ -60,13 +60,6 @@ export function setActTimeout(fn: () => void, ms?: number) {
60
60
  }, ms)
61
61
  }
62
62
 
63
- /**
64
- * Assert the parameter is of a specific type.
65
- */
66
- export function expectType<T>(_: T): void {
67
- return undefined
68
- }
69
-
70
63
  /**
71
64
  * Assert the parameter is not typed as `any`
72
65
  */
@@ -8,12 +8,16 @@ import {
8
8
  createComputed,
9
9
  createMemo,
10
10
  createResource,
11
+ createSignal,
12
+ mergeProps,
11
13
  on,
12
14
  onCleanup,
15
+ untrack,
13
16
  } from 'solid-js'
14
17
  import { createStore, reconcile, unwrap } from 'solid-js/store'
15
18
  import { useQueryClient } from './QueryClientProvider'
16
19
  import { shouldThrowError } from './utils'
20
+ import { useIsRestoring } from './isRestoring'
17
21
  import type { CreateBaseQueryOptions } from './types'
18
22
  import type { Accessor } from 'solid-js'
19
23
  import type { QueryClient } from './QueryClient'
@@ -104,18 +108,31 @@ export function createBaseQuery<
104
108
  | QueryObserverResult<TData, TError>
105
109
 
106
110
  const client = createMemo(() => useQueryClient(queryClient?.()))
111
+ const isRestoring = useIsRestoring()
107
112
 
108
- const defaultedOptions = client().defaultQueryOptions(options())
109
- defaultedOptions._optimisticResults = 'optimistic'
110
- defaultedOptions.structuralSharing = false
111
- if (isServer) {
112
- defaultedOptions.retry = false
113
- defaultedOptions.throwOnError = true
114
- }
115
- const observer = new Observer(client(), defaultedOptions)
113
+ const defaultedOptions = createMemo(() =>
114
+ mergeProps(client().defaultQueryOptions(options()), {
115
+ get _optimisticResults() {
116
+ return isRestoring() ? 'isRestoring' : 'optimistic'
117
+ },
118
+ structuralSharing: false,
119
+ ...(isServer && { retry: false, throwOnError: true }),
120
+ }),
121
+ )
122
+
123
+ const [observer, setObserver] = createSignal(
124
+ new Observer(client(), untrack(defaultedOptions)),
125
+ )
126
+ // we set the value in a computed because `createMemo`
127
+ // returns undefined during transitions
128
+ createComputed(
129
+ on(client, (c) => setObserver(new Observer(c, defaultedOptions())), {
130
+ defer: true,
131
+ }),
132
+ )
116
133
 
117
134
  const [state, setState] = createStore<QueryObserverResult<TData, TError>>(
118
- observer.getOptimisticResult(defaultedOptions),
135
+ observer().getOptimisticResult(defaultedOptions()),
119
136
  )
120
137
 
121
138
  const createServerSubscriber = (
@@ -124,9 +141,9 @@ export function createBaseQuery<
124
141
  ) => void,
125
142
  reject: (reason?: any) => void,
126
143
  ) => {
127
- return observer.subscribe((result) => {
144
+ return observer().subscribe((result) => {
128
145
  notifyManager.batchCalls(() => {
129
- const query = observer.getCurrentQuery()
146
+ const query = observer().getCurrentQuery()
130
147
  const unwrappedResult = hydrateableObserverResult(query, result)
131
148
 
132
149
  if (unwrappedResult.isError) {
@@ -139,32 +156,30 @@ export function createBaseQuery<
139
156
  }
140
157
 
141
158
  const createClientSubscriber = () => {
142
- return observer.subscribe((result) => {
159
+ const obs = observer()
160
+ return obs.subscribe((result) => {
143
161
  notifyManager.batchCalls(() => {
144
162
  // @ts-expect-error - This will error because the reconcile option does not
145
163
  // exist on the query-core QueryObserverResult type
146
- const reconcileOptions = observer.options.reconcile
164
+ const reconcileOptions = obs.options.reconcile
165
+
166
+ setState((store) => {
167
+ return reconcileFn(
168
+ store,
169
+ result,
170
+ reconcileOptions === undefined ? 'id' : reconcileOptions,
171
+ )
172
+ })
147
173
  // If the query has data we dont suspend but instead mutate the resource
148
174
  // This could happen when placeholderData/initialData is defined
149
- if (queryResource()?.data && result.data && !queryResource.loading) {
150
- setState((store) => {
151
- return reconcileFn(
152
- store,
153
- result,
154
- reconcileOptions === undefined ? 'id' : reconcileOptions,
155
- )
156
- })
175
+ if (
176
+ queryResource()?.data &&
177
+ result.data &&
178
+ !queryResource.loading &&
179
+ isRestoring()
180
+ )
157
181
  mutate(state)
158
- } else {
159
- setState((store) => {
160
- return reconcileFn(
161
- store,
162
- result,
163
- reconcileOptions === undefined ? 'id' : reconcileOptions,
164
- )
165
- })
166
- refetch()
167
- }
182
+ else refetch()
168
183
  })()
169
184
  })
170
185
  }
@@ -178,17 +193,16 @@ export function createBaseQuery<
178
193
  ResourceData | undefined
179
194
  >(
180
195
  () => {
196
+ const obs = observer()
181
197
  return new Promise((resolve, reject) => {
182
- if (isServer) {
183
- unsubscribe = createServerSubscriber(resolve, reject)
184
- } else {
185
- if (!unsubscribe) {
186
- unsubscribe = createClientSubscriber()
187
- }
188
- }
198
+ if (isServer) unsubscribe = createServerSubscriber(resolve, reject)
199
+ else if (!unsubscribe && !isRestoring())
200
+ unsubscribe = createClientSubscriber()
201
+
202
+ obs.updateResult()
189
203
 
190
- if (!state.isLoading) {
191
- const query = observer.getCurrentQuery()
204
+ if (!state.isLoading && !isRestoring()) {
205
+ const query = obs.getCurrentQuery()
192
206
  resolve(hydrateableObserverResult(query, state))
193
207
  }
194
208
  })
@@ -197,7 +211,9 @@ export function createBaseQuery<
197
211
  initialValue: state,
198
212
 
199
213
  // If initialData is provided, we resolve the resource immediately
200
- ssrLoadFrom: options().initialData ? 'initial' : 'server',
214
+ get ssrLoadFrom() {
215
+ return options().initialData ? 'initial' : 'server'
216
+ },
201
217
 
202
218
  get deferStream() {
203
219
  return options().deferStream
@@ -212,37 +228,50 @@ export function createBaseQuery<
212
228
  * Note that this is only invoked on the client, for queries that were originally run on the server.
213
229
  */
214
230
  onHydrated(_k, info) {
231
+ const defaultOptions = defaultedOptions()
215
232
  if (info.value) {
216
233
  hydrate(client(), {
217
234
  queries: [
218
235
  {
219
- queryKey: defaultedOptions.queryKey,
220
- queryHash: defaultedOptions.queryHash,
236
+ queryKey: defaultOptions.queryKey,
237
+ queryHash: defaultOptions.queryHash,
221
238
  state: info.value,
222
239
  },
223
240
  ],
224
241
  })
225
242
  }
226
243
 
227
- if (!unsubscribe) {
228
- /**
229
- * Do not refetch query on mount if query was fetched on server,
230
- * even if `staleTime` is not set.
231
- */
232
- const newOptions = { ...defaultedOptions }
233
- if (defaultedOptions.staleTime || !defaultedOptions.initialData) {
234
- newOptions.refetchOnMount = false
235
- }
236
- // Setting the options as an immutable object to prevent
237
- // wonky behavior with observer subscriptions
238
- observer.setOptions(newOptions)
239
- setState(observer.getOptimisticResult(newOptions))
240
- unsubscribe = createClientSubscriber()
244
+ if (unsubscribe) return
245
+ /**
246
+ * Do not refetch query on mount if query was fetched on server,
247
+ * even if `staleTime` is not set.
248
+ */
249
+ const newOptions = { ...defaultOptions }
250
+ if (defaultOptions.staleTime || !defaultOptions.initialData) {
251
+ newOptions.refetchOnMount = false
241
252
  }
253
+ // Setting the options as an immutable object to prevent
254
+ // wonky behavior with observer subscriptions
255
+ observer().setOptions(newOptions)
256
+ setState(observer().getOptimisticResult(newOptions))
257
+ unsubscribe = createClientSubscriber()
242
258
  },
243
259
  },
244
260
  )
245
261
 
262
+ createComputed(
263
+ on(
264
+ [isRestoring, observer],
265
+ ([restoring]) => {
266
+ const unsub = unsubscribe
267
+ queueMicrotask(() => unsub?.())
268
+ unsubscribe = null
269
+ if (!restoring) refetch()
270
+ },
271
+ { defer: true },
272
+ ),
273
+ )
274
+
246
275
  onCleanup(() => {
247
276
  if (unsubscribe) {
248
277
  unsubscribe()
@@ -252,8 +281,11 @@ export function createBaseQuery<
252
281
 
253
282
  createComputed(
254
283
  on(
255
- () => client().defaultQueryOptions(options()),
256
- () => observer.setOptions(client().defaultQueryOptions(options())),
284
+ [observer, defaultedOptions],
285
+ ([obs, opts]) => {
286
+ obs.setOptions(opts)
287
+ setState(obs.getOptimisticResult(opts))
288
+ },
257
289
  {
258
290
  // Defer because we don't need to trigger on first render
259
291
  // This only cares about changes to options after the observer is created
@@ -266,12 +298,14 @@ export function createBaseQuery<
266
298
  on(
267
299
  () => state.status,
268
300
  () => {
301
+ const obs = observer()
269
302
  if (
270
303
  state.isError &&
271
304
  !state.isFetching &&
272
- shouldThrowError(observer.options.throwOnError, [
305
+ !isRestoring() &&
306
+ shouldThrowError(obs.options.throwOnError, [
273
307
  state.error,
274
- observer.getCurrentQuery(),
308
+ obs.getCurrentQuery(),
275
309
  ])
276
310
  ) {
277
311
  throw state.error
@@ -1,5 +1,5 @@
1
1
  import { MutationObserver } from '@tanstack/query-core'
2
- import { createComputed, on, onCleanup } from 'solid-js'
2
+ import { createComputed, createMemo, on, onCleanup } from 'solid-js'
3
3
  import { createStore } from 'solid-js/store'
4
4
  import { useQueryClient } from './QueryClientProvider'
5
5
  import { shouldThrowError } from './utils'
@@ -22,10 +22,10 @@ export function createMutation<
22
22
  options: CreateMutationOptions<TData, TError, TVariables, TContext>,
23
23
  queryClient?: Accessor<QueryClient>,
24
24
  ): CreateMutationResult<TData, TError, TVariables, TContext> {
25
- const client = useQueryClient(queryClient?.())
25
+ const client = createMemo(() => useQueryClient(queryClient?.()))
26
26
 
27
27
  const observer = new MutationObserver<TData, TError, TVariables, TContext>(
28
- client,
28
+ client(),
29
29
  options(),
30
30
  )
31
31